Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df51e17198 |
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
.gradle/
|
||||
__pycache__/
|
||||
**/build/
|
||||
**/run/
|
||||
**/out/
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.25 — 2026-09-11
|
||||
|
||||
- Nouveau générateur `sanctuary:island_v25` pour l’île initiale : massifs
|
||||
dispersés de calcite, cavités reliées, refuges à champignons, lacs
|
||||
souterrains d’eau et de lave selon la roche disponible. Les filons suivent
|
||||
la profondeur relative ; les rivières cherchent des sorties vers le vide
|
||||
sans imposer de cascade à chaque extrémité.
|
||||
- Marais attachés aux eaux et berges existantes, en surface et sous l’île.
|
||||
Plusieurs strates peuvent se superposer en X/Z, avec des limites verticales
|
||||
propres, des sols humides et de la végétation locale. Les biomes des autres
|
||||
étages et l’arrivée naturelle en surface restent préservés.
|
||||
- Recherche conditionnelle d’un village abandonné souterrain, d’une mine et
|
||||
de sa cabane, d’une hutte et de salles à mobs. Les placements inadaptés
|
||||
restent refusés ; ces structures ne sont pas garanties sur chaque graine.
|
||||
Les secrets et les cinq lieux aériens restent soumis à leurs options.
|
||||
- Escaliers du sanctuaire minéral raccordés et cadre central de blackstone
|
||||
de 7 × 9 blocs, ouverture de 5 × 7, inactif. Aucun nouveau portail
|
||||
fonctionnel ni nouvelle dimension n’est activé.
|
||||
- Monument vanilla garanti pour chaque nouvelle expansion océanique de génération 25
|
||||
**acceptée**, de 512 ou 1 024 blocs, avec les structures Minecraft activées.
|
||||
Son départ et ses pièces sont enregistrés avant réservation ; une recherche
|
||||
infructueuse refuse la demande. La référence d’une carte d’exploration marque désormais son
|
||||
unique chunk gagnant à sauvegarder, sans transformer une recherche en visite.
|
||||
|
||||
Les tailles 512 / 724 / 1 024 et Moyen par défaut sont conservés. Un **nouveau
|
||||
monde Sanctuary** est nécessaire. Les essais portent sur des mondes neufs ;
|
||||
la reprise des anciens mondes ne fait pas partie de cette livraison.
|
||||
|
||||
`check build` et `assemblePack` passent. Le JAR, le MRpack et le ZIP Prism
|
||||
sont vérifiés localement ; leur publication est en cours.
|
||||
Les preuves acquises, dont les six créations et six réouvertures de monuments,
|
||||
sont détaillées avec leur périmètre dans les [essais alpha.25](docs/testing-alpha25.md).
|
||||
Voir aussi le [contrat de génération](docs/generation-alpha25.md) et les
|
||||
[mesures de performances](docs/performance-alpha25.md) : aucun gain global
|
||||
de chargement n’est établi. Essais natifs sur Mac ; aucun essai Windows natif
|
||||
n’est revendiqué. L’alpha.24 reste publiée ; aucune publication ou
|
||||
synchronisation Prism de l’alpha.25 n’est annoncée à ce stade.
|
||||
|
||||
## 0.1.0-alpha.20 — 2026-09-10
|
||||
|
||||
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
|
||||
|
||||
@@ -15,9 +15,48 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
|
||||
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
|
||||
conserve les intentions ; le [backlog](docs/backlog.md) suit les
|
||||
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.24 est
|
||||
publiée** : cinq lieux suspendus accompagnent l’île naturelle et ses secrets.
|
||||
Voir [les essais](docs/testing-alpha24.md) et [la distribution](docs/packwiz.md).
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.25 est construite et vérifiée localement** : elle retravaille les reliefs et les profondeurs de
|
||||
Sanctuary Island. **L’alpha.24 reste la livraison publiée** sur le canal
|
||||
stable ; aucune synchronisation Prism pour l’alpha.25 n’est encore annoncée. Voir
|
||||
[les essais alpha.25](docs/testing-alpha25.md) et [la distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.25 — reliefs et profondeurs
|
||||
|
||||
L’île initiale reçoit des massifs dispersés de calcite, des cavités reliées
|
||||
par des galeries sinueuses, des refuges à champignons et des propositions
|
||||
de lacs souterrains d’eau et de lave. Leurs fonds et berges doivent tenir
|
||||
dans la roche existante. Les minerais suivent la profondeur relative de
|
||||
l’île ; les sorties de rivière peuvent s’ouvrir sur ses flancs quand le
|
||||
terrain s’y prête.
|
||||
|
||||
Les marais suivent de vraies eaux calmes et leurs berges, **en surface et
|
||||
sous l’île**, avec boue, argile et végétation locale. Plusieurs strates
|
||||
peuvent occuper le même emplacement horizontal à des hauteurs différentes.
|
||||
Leur biome reste dans chaque bande habitable, sans remplacer toute la
|
||||
colonne de grottes ni créer un plan d’eau artificiel. L’arrivée reste
|
||||
naturelle en surface.
|
||||
|
||||
Village abandonné souterrain, mine, cabane de mineur, hutte et salles à mobs
|
||||
sont recherchés **sans garantie de présence**. Les secrets et les cinq lieux
|
||||
suspendus conservent leurs options de génération. Le sanctuaire minéral
|
||||
reçoit des escaliers raccordés et un cadre central de blackstone inactif.
|
||||
Avec les structures Minecraft activées, chaque nouvelle expansion océanique de génération 25
|
||||
acceptée de **512 ou 1 024 blocs** prévoit un monument vanilla avant sa
|
||||
réservation ; faute d’emplacement adapté, la demande est refusée.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour `sanctuary:island_v25`. Les diamètres
|
||||
512 / 724 / 1 024, **Moyen par défaut**, le bouton **Personnaliser**, la hauteur
|
||||
de 384 blocs et les nuages sont conservés. Les essais portent sur des mondes
|
||||
neufs ; la reprise des anciens mondes ne fait pas partie de cette livraison.
|
||||
Le cadre minéral n’ouvre pas de nouvelle dimension.
|
||||
|
||||
`check build` et l’assemblage passent sur Mac. Le JAR, le MRpack et le ZIP
|
||||
Prism sont vérifiés ; la publication et la synchronisation sont en cours.
|
||||
Aucune validation Windows native n’est revendiquée. Le [contrat de génération](docs/generation-alpha25.md),
|
||||
les [preuves et limites](docs/testing-alpha25.md) et les
|
||||
[mesures de performances](docs/performance-alpha25.md) précisent la portée
|
||||
de chaque résultat. Aucun gain global de chargement face à l’alpha.24
|
||||
n’est établi.
|
||||
|
||||
## Alpha.24 — les lieux suspendus
|
||||
|
||||
@@ -552,7 +591,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.23 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.25 — en validation locale |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -576,12 +615,13 @@ cd sanctuary-beta
|
||||
qui charge par défaut le nouveau générateur unifié Sanctuary dans un monde
|
||||
de développement neuf. Les suites historiques utilisent explicitement
|
||||
`-PsanctuaryUnifiedTests=false`.
|
||||
Les scénarios couverts et les essais multijoueurs restant à effectuer sont
|
||||
décrits dans [Validation](docs/testing.md).
|
||||
Les scénarios actuels et leurs limites sont décrits dans les
|
||||
[essais alpha.25](docs/testing-alpha25.md), complétés par le
|
||||
[guide général de validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.25.jar` : mod de développement à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
# Alpha.25 — reliefs et profondeurs de Sanctuary
|
||||
|
||||
## Portée et sauvegardes
|
||||
|
||||
La génération `sanctuary:island_v25` concerne les **nouveaux mondes Sanctuary**.
|
||||
Les trois diamètres nominaux restent **512 / 724 / 1 024 blocs**, avec **Moyen**
|
||||
par défaut et le bouton Minecraft **Personnaliser**. Le monde conserve sa
|
||||
hauteur de 384 blocs et les nuages existants. Les sommets naturels peuvent
|
||||
approcher Y358 ; cette borne n'est pas une hauteur imposée à chaque massif.
|
||||
|
||||
Les codecs et réglages historiques restent enregistrés. Le preset24 est
|
||||
conservé sous `sanctuary:sanctuary_v24` ; charger une sauvegarde24 ne transforme
|
||||
pas son générateur en25. Aucun chunk existant n'est régénéré, aucun monde
|
||||
personnel n'est migré et aucun portail vers une nouvelle dimension n'est activé.
|
||||
La validation demandée porte désormais uniquement sur les **nouveaux mondes
|
||||
de test** : la compatibilité avec les anciennes versions n'est plus un
|
||||
critère de livraison de cette alpha. Les preuves historiques déjà produites
|
||||
restent documentées, sans prolonger cette campagne de compatibilité.
|
||||
|
||||
Les modifications de relief, de cavités et de géologie décrites ci-dessous
|
||||
s'appliquent à **l'île initiale uniquement**. Les continents d'expansion
|
||||
conservent leurs champs de terrain et leurs océans précédents ; la nouveauté
|
||||
pour leurs océans est le contrat de monument détaillé plus bas.
|
||||
|
||||
## Relief, cavités et ressources
|
||||
|
||||
Le terrain initial utilise les réglages distincts `unified_25_5`,
|
||||
`unified_25_10` et `unified_25_20`. Des groupes de pics dispersés étirent
|
||||
verticalement les masses existantes, avec de la calcite dans leurs hauteurs.
|
||||
L'étirement reste monotone et s'atténue à l'arrivée ; il ne crée pas un plateau
|
||||
central artificiel. L'emprise horizontale initiale reste bornée par la taille
|
||||
choisie. Aucun bambou supplémentaire n'est associé aux massifs de calcite.
|
||||
|
||||
Un plan déterministe ajoute des cavités sèches, des refuges à champignons et
|
||||
des propositions de lacs souterrains, ainsi que des chambres basses de lave.
|
||||
Les chambres d'un même groupe sont reliées par des galeries sinueuses ; les
|
||||
failles courtes dérivent avec la hauteur. Les cavités existent indépendamment
|
||||
du choix de placer un village ou un donjon. Elles ne garantissent pas qu'une
|
||||
structure y trouvera la place nécessaire.
|
||||
|
||||
Les cavités sèches cherchent désormais leur assise dans la masse rocheuse
|
||||
intacte. `DryFit` examine au maximum **48 propositions** de position et de
|
||||
profondeur, sans modifier leurs rayons et sans consulter une structure ou un
|
||||
village. Un premier contrôle de sol et de voûte précède la vérification dense
|
||||
de **trois couches de roche existante** sous toute l'empreinte ouverte du
|
||||
plancher. Les bassins et leurs membranes passent en priorité. Un candidat
|
||||
refusé conserve sa proposition de cavité, avec un résultat de certification
|
||||
négatif explicite : il n'obtient aucune semelle artificielle.
|
||||
|
||||
Une assise certifiée est conservée contre les seules nouvelles excavations,
|
||||
connexions et fractures25. Cette membrane retire des coupes prévues ; elle
|
||||
n'ajoute jamais de roche dans un vide du champ naturel. Le certificat du
|
||||
plancher n'est ni une certification de toute la voûte ni une admission de
|
||||
village. Les coupes hydrologiques héritées, les secrets, les accès et le
|
||||
volume complet des pièces doivent encore passer les contrôles de structures.
|
||||
Les diagnostics exposent pour chaque cavité sèche le nombre de propositions,
|
||||
le statut du plancher et le motif final de refus éventuel.
|
||||
|
||||
Les lacs ne sont remplis qu'après admission de leur fond et de leurs berges
|
||||
dans la densité sculptée. Une proposition non retenue reste une cavité sèche.
|
||||
Les plans hérités d'eau et de lave participent également aux exclusions.
|
||||
Il n'y a ni couche d'océan globale sous l'île, ni obligation de remplir tous
|
||||
les volumes creusés. Les profondeurs de cette première passe sont de cinq
|
||||
blocs pour les nouveaux lacs d'eau et de trois pour les bassins de lave.
|
||||
|
||||
Les refuges combinent mycélium, mousse et podzol. Ils reçoivent des tentatives
|
||||
de grands champignons vanilla et de mooshrooms aux emplacements habitables.
|
||||
Leur quantité effective dépend du terrain, de la place disponible et des
|
||||
décorations voisines. Les biomes `terrain25_*` gardent les forêts tempérées,
|
||||
les grottes luxuriantes, les dripstones et le sulfure.
|
||||
|
||||
Le marais suit certaines eaux calmes et leurs berges réelles, **en surface
|
||||
comme sous l'île**. Il remplace le secteur elliptique à altitude fixe.
|
||||
La recherche de surface examine au plus 64 candidats par région, sur les
|
||||
étangs et lacs déjà retenus, et garde au plus trois petites poches. Une
|
||||
seconde recherche indépendante examine au plus 64 candidats pour toute
|
||||
l'île et garde au plus trois poches sur les lacs souterrains déjà admis.
|
||||
Ce plan inférieur est partagé par les régions, sans nouveau tirage selon
|
||||
le chunk demandé. La sélection n'est calculée qu'une fois par contexte de
|
||||
monde ; les régions combinent ensuite son index avec leur sélection de
|
||||
surface. Un site inférieur peut donc figurer dans plusieurs diagnostics
|
||||
régionaux : son identifiant doit être dédupliqué pour compter les lieux,
|
||||
et les compteurs régionaux ne sont pas à additionner comme des recherches
|
||||
indépendantes. Les rivières, terrasses, cascades, écoulements chauds et
|
||||
refuges à mooshrooms restent exclus de cette sélection.
|
||||
|
||||
Le marais ne crée aucun plan d'eau, cavité ou plateforme pour une hutte.
|
||||
Il retient des fonds existants de un à quatre blocs en surface, jusqu'à
|
||||
six blocs dans les lacs inférieurs, et des berges sur trois couches
|
||||
rocheuses, entre le niveau d'eau et quatre blocs au-dessus. Les voisins
|
||||
sont raccordés par des pas d'un bloc au plus. Une poche exige au moins
|
||||
douze colonnes d'eau et quarante-huit colonnes de berge en surface,
|
||||
trente-deux en sous-sol ; une hutte peut néanmoins être refusée par ses
|
||||
propres contrôles de hauteur, largeur, profondeur d'eau et pilotis.
|
||||
|
||||
Une poche couvre au maximum un rayon de 28 blocs ; ses berges s'étendent
|
||||
jusqu'à 18 pas d'une eau réelle. Le biome possède une épaisseur locale :
|
||||
deux blocs sous le fond humide ou trois sous le sol sec, jusqu'à douze
|
||||
blocs au-dessus de l'eau ou du sol. En sous-sol, elle s'arrête avant le
|
||||
premier plafond rocheux et doit conserver au moins quatre blocs libres.
|
||||
Plusieurs marais peuvent se superposer au même X/Z : chaque colonne garde
|
||||
son niveau d'eau et l'identité de son bassin. Le biome est l'union de ces
|
||||
petites enveloppes, jamais une colonne continue traversant tous les étages.
|
||||
Les grottes hors de ces enveloppes gardent leurs biomes. Le rayon de
|
||||
96 blocs autour de l'arrivée et les massifs sont exclus **en surface** ;
|
||||
un lac inférieur peut se trouver sous leur X/Z sans changer l'arrivée ni
|
||||
les sommets. Les réservations d'écoulement restent préservées. Le terrain existant reçoit
|
||||
des plages de terre humide, de boue et d'argile, sans déplacer les surfaces.
|
||||
Le même plan immuable fournit les colonnes de terrain, le biome 3D et les
|
||||
candidats de hutte ; il est indépendant des options de structures. Les
|
||||
couleurs et la flore vanilla du marais sont conservées, sans ses placements
|
||||
supplémentaires de lave, sources ou fossiles à altitude absolue.
|
||||
|
||||
Les placements vanilla fondés sur la surface ne suffisant pas sous une
|
||||
voûte, une petite passe botanique utilise directement les berges inférieures.
|
||||
Elle tente des herbes, orchidées bleues, buissons à lucioles, petits
|
||||
champignons, cannes à sucre, nénuphars et lianes. Les plantes nécessitent
|
||||
de l'air et leur condition native de survie ; les lianes commencent sur
|
||||
une vraie paroi ou un plafond. Les écritures restent dans le chunk courant
|
||||
et ne remplacent ni sol, ni eau, ni construction. Aucun arbre n'est forcé
|
||||
pour remplir une cavité. La quantité visible est mesurée dans les témoins
|
||||
natifs ; un candidat de plante ne constitue pas une plante effectivement
|
||||
générée.
|
||||
|
||||
Pour les petits champignons de ces marais, le contrôle natif de survie est
|
||||
complété avant l'éclairage final : une canopée partielle peut laisser arriver
|
||||
un niveau de lumière 13 qui n'était pas encore calculé pendant la décoration.
|
||||
Une garde conservative inspecte au plus treize colonnes de ciel et vingt-cinq
|
||||
cellules d'émission dans un rayon Manhattan de deux blocs. Elle ne simule ni
|
||||
ne déclenche l'éclairage ; les obstacles sur les courts trajets de propagation
|
||||
sont ignorés pour ne pas sous-estimer la lumière. Les champignons réellement
|
||||
abrités et les substrats bénéficiant de l'exemption lumineuse vanilla restent
|
||||
possibles. Cette garde est limitée aux petits champignons des marais25.
|
||||
|
||||
Le test pur vérifie notamment deux lacs sous une même colonne, la séparation
|
||||
de leurs enveloppes, l'ordre de recherche inversé, les appuis et les refus
|
||||
du vide et de la lave. Compilation et tests purs passent après cette
|
||||
extension. Le témoin natif **512 / graine 0** valide deux poches inférieures
|
||||
à Y160 et Y170, leurs sols et 38 blocs de végétation réellement présents
|
||||
sous roche. Les témoins antérieurs limités aux marais de surface restent
|
||||
distingués de cette vérification dans les [essais de l'alpha.25](testing-alpha25.md).
|
||||
La présence d'une hutte demeure conditionnelle ; ce témoin n'en admet aucune.
|
||||
|
||||
Les nouveaux filons se mesurent à la **profondeur relative de la colonne
|
||||
naturelle intacte**, entre son sommet et sa base, et non à la hauteur du sol
|
||||
d'une grotte. Charbon, fer et cuivre accompagnent les couches accessibles ;
|
||||
or, lapis, redstone et émeraude ont leurs bandes de profondeur. Les nouvelles
|
||||
poches de diamant sont recherchées au-delà de 65 % de profondeur relative.
|
||||
Ce sont des réserves physiques finies, sans quota garanti par minerai. Les
|
||||
filons propres aux anciens secrets et aux îlots aériens conservent leur
|
||||
distribution locale distincte.
|
||||
|
||||
## Rivières ouvertes et réservation des écoulements
|
||||
|
||||
Pour chaque extrémité admissible d'une rivière, la génération cherche une
|
||||
sortie proche sur 32 directions, entre 8 et 64 blocs. Elle conserve un lit
|
||||
supporté et des berges jusqu'à la lèvre. La sortie peut ensuite s'ouvrir sur
|
||||
le flanc de l'île et laisser l'eau tomber dans le vide suivant les règles
|
||||
Minecraft. Aucune paire de sorties n'est forcée : une rivière peut avoir
|
||||
zéro, une ou deux extrémités adaptées.
|
||||
|
||||
Les réservations d'expansion sont établies avant l'exploration. Pour les
|
||||
nouvelles sorties initiales25, elles couvrent les cellules du chenal et un
|
||||
élargissement local de sept blocs pour la propagation de l'eau, de Y0 à son
|
||||
niveau de départ. Elles sont projetées sur les chunks parce qu'une expansion
|
||||
génère des colonnes complètes. Ce masque ne réserve pas tout le rectangle
|
||||
englobant une diagonale ni une nouvelle couronne autour de l'île.
|
||||
|
||||
## Secrets, lieux aériens et nouvelles structures
|
||||
|
||||
La grande traversée, la salle d'expansion et les secrets conservés utilisent
|
||||
le terrain25 pour chercher leurs appuis. Le sanctuaire minéral garde son
|
||||
volume de 79 × 73 × 27 blocs et sa chambre centrale. Son nouveau rendu résout
|
||||
les angles des escaliers en pierre et en cobblestone avant le découpage par
|
||||
chunk. Un cadre central de blackstone de **7 × 9 blocs**, ouvert sur
|
||||
**5 × 7 blocs**, reste **inactif**. Il ne contient aucun bloc de portail et
|
||||
ne rend pas la blackstone utilisable comme cadre Nether ailleurs.
|
||||
|
||||
Les cinq lieux aériens24 restent présents dans le plan initial lorsqu'ils
|
||||
sont activés : montgolfière, deux navires marchands et deux îlots minéraux,
|
||||
dont un avec cascade. Leur géométrie et leurs limites propres restent
|
||||
inchangées ; les points de rivage cherchent les secteurs compatibles avec
|
||||
le nouveau relief. La recherche reste bornée et échoue explicitement si
|
||||
elle ne peut pas établir les cinq sites, au lieu d'en omettre un en silence.
|
||||
|
||||
La nouvelle sélection souterraine recherche, sans les forcer :
|
||||
|
||||
- au plus un village abandonné étendu dans une cavité sèche, avec les pièces
|
||||
et habitants vanilla des variantes abandonnées ;
|
||||
- une mine native, éventuellement accompagnée d'une petite cabane de mineur
|
||||
et d'un raccord réellement praticable ;
|
||||
- une hutte native dans le secteur de marais, à proximité d'eau ;
|
||||
- jusqu'à trois salles de monstres utilisant la feature vanilla, ses
|
||||
conditions d'ouverture, ses coffres et son spawner.
|
||||
|
||||
Le village recherche au moins six bâtiments et un accès vers une grotte
|
||||
naturelle. Les fondations, la couverture rocheuse, les fluides et les
|
||||
réservations des secrets sont vérifiés avant son admission. Les ajustements
|
||||
de sol et de voûte restent dans les volumes acceptés. La sélection générale
|
||||
des structures natives ne rajoute pas de village de surface sur l'île25 ;
|
||||
les villages des expansions gardent leur catalogue.
|
||||
|
||||
Les départs et les pièces sont enregistrés comme structures natives. Les
|
||||
salles de monstres restent des features et sont recensées séparément. Le
|
||||
chargement d'un chunk terminé ne reconstruit pas son mobilier, son butin ni
|
||||
ses habitants retirés. Les structures facultatives refusées sont indiquées
|
||||
dans les diagnostics de planification ; leur absence peut être normale.
|
||||
|
||||
## Ordre de préparation et options
|
||||
|
||||
Avant de demander les nouveaux chunks, le serveur prépare le contexte de
|
||||
terrain25 et ses plans de cavités/hydrologie, puis les réservations des
|
||||
secrets existants. Le plan aérien s'appuie sur ces colonnes finales et ces
|
||||
protections. Son manifeste immuable est lié au journal25. La sélection
|
||||
souterraine25 vient ensuite : elle respecte les secrets et les sites déjà
|
||||
réservés. Les projections de sol admises sont partagées entre les colonnes
|
||||
consultées par les pièces natives et la production du terrain.
|
||||
|
||||
Les options **Générer les structures** et **Structures expérimentales**
|
||||
contrôlent les secrets expérimentaux, les satellites et les nouveaux lieux
|
||||
souterrains. Leur désactivation n'enlève pas le terrain naturel, les cavités,
|
||||
les lacs, les minerais ou leur végétation. Les structures natives générales
|
||||
restent soumises à l'option Minecraft **Générer les structures**. La grande
|
||||
traversée et la salle d'expansion conservent leur règle antérieure : elles
|
||||
restent recherchées avec cette option Minecraft active, même si les structures
|
||||
expérimentales sont désactivées. Ces options sont enregistrées avec le monde.
|
||||
|
||||
Sans structures expérimentales, le catalogue général conserve ses candidats
|
||||
de mines et de huttes de surface ; les salles de monstres des biomes restent
|
||||
également possibles. Les recherches supplémentaires du plan25 sont désactivées,
|
||||
notamment les huttes des marais inférieurs à hauteur locale, la sélection de
|
||||
mine accompagnée d'une cabane et les salles complémentaires planifiées.
|
||||
La présence des mêmes structures dans les deux configurations n'est donc pas
|
||||
garantie.
|
||||
|
||||
Les calculs naturels sont dérivés de la graine, de la taille et du générateur.
|
||||
Leurs caches bornés ne dépendent pas de l'ordre de visite : cache de relief
|
||||
par thread, cache de 32 768 colonnes naturelles par contexte et plans
|
||||
hydrologiques régionaux réutilisés. La sélection souterraine limite ses
|
||||
candidats et ses sondages ; un budget épuisé refuse la famille concernée.
|
||||
Ces bornes n'impliquent pas une durée de chargement garantie.
|
||||
|
||||
Les recherches du sommet et de la base naturels excluent des altitudes dont
|
||||
la densité est mathématiquement non positive. L'érosion inférieure exclut
|
||||
**Y1–23** ; l'érosion supérieure exclut les coordonnées ramenées à
|
||||
`naturalY >= 304`, et le fondu25 exclut Y358 et au-delà. Le balayage commence
|
||||
ainsi au plus à **Y303 sans étirement**, ou **Y357 avec relief**, puis s'arrête
|
||||
au plus bas à Y24. Les arrondis conservent exactement le calcul de
|
||||
`naturalY`. Les bosquets réutilisent ce sommet naturel mis en cache comme
|
||||
plafond supplémentaire de leur recherche ; les nouvelles cavités ne peuvent
|
||||
que retirer de la matière sous ce plafond. Les hauteurs trouvées et les
|
||||
profondeurs utilisées pour les filons restent identiques.
|
||||
|
||||
La matrice native Mac du 11 septembre 2026, après `DryFit`, vérifie ces bornes
|
||||
sur **864 colonnes** et **2 592 plafonds de recherche des bosquets**. Les
|
||||
résultats des anciens et nouveaux balayages sont identiques : 253 651 contre
|
||||
197 616 sondages pour les extrema naturels (**−22,1 %**), et 223 403 contre 6 501
|
||||
pour les recherches de bosquets (**−97,1 %**). Ces nombres concernent ces
|
||||
boucles précises, pas une réduction mesurée du chargement complet du monde.
|
||||
La même matrice valide 16 584 positions de sol après les nouvelles coupes :
|
||||
**26 planchers certifiés sur 33 propositions sèches**. Les grandes cavités
|
||||
sèches sont certifiées sur 512/graine 0 et 512/graine 42 ; elles restent refusées
|
||||
sur les sept autres témoins. Ces contrôles utilisent les samplers natifs et
|
||||
ne constituent pas des essais de blocs chargés, de fluides sous ticks ou de
|
||||
villages fonctionnels. Ils n'apportent aucune validation Windows.
|
||||
|
||||
## Monument garanti des nouvelles expansions océaniques25
|
||||
|
||||
Avec **Générer les structures** activé, toute nouvelle expansion25 de type
|
||||
`ocean`, de diamètre **512 ou 1 024**, doit trouver un monument natif adapté
|
||||
**avant sa réservation**. La recherche s'inscrit dans le délai d'admission
|
||||
existant de 30 secondes. Si aucun départ ne convient, la demande est refusée
|
||||
sans publier cette expansion. Une expansion non océanique garde l'admission
|
||||
d'au moins une structure native adaptée, avec une priorité souple aux
|
||||
familles absentes des régions réellement vérifiées.
|
||||
|
||||
Le départ exact du monument, ses pièces, son enveloppe, son algorithme et
|
||||
son empreinte SHA-256 sont figés dans `structure_plan25`. Le journal25 utilise
|
||||
le schéma3 et inclut ces données dans l'identité du certificat de réemploi
|
||||
du vide. Les champs restent absents des descriptors historiques. La reprise
|
||||
valide et restaure ce départ ; elle ne retire pas un autre monument au hasard.
|
||||
|
||||
Le même départ sert à la génération réelle et à la localisation. Son volume
|
||||
protégé empêche d'autres départs de le recouvrir. Le comportement des pièces,
|
||||
des gardiens et des grands gardiens reste celui de Minecraft. Le plan n'est
|
||||
pas un mécanisme de restauration après destruction. Une expansion encore
|
||||
réservée ne devient pas localisable comme si sa préparation était terminée :
|
||||
la localisation attend son véritable état `ready`.
|
||||
|
||||
Lorsqu'une carte d'exploration sélectionne un départ avec `skipReferenced`,
|
||||
le compteur natif est incrémenté par `addReference`, puis **seul le chunk du
|
||||
départ gagnant** est marqué à sauvegarder. Cela permet de conserver la
|
||||
référence même si ce chunk distant était déjà sauvegardé. Les autres départs
|
||||
ne sont pas référencés ; une recherche ordinaire n'incrémente aucun compteur.
|
||||
Ces références servent à la sélection des cartes et ne représentent pas des
|
||||
visites de joueurs. Le correctif n'ajoute aucune migration et ne reconstruit
|
||||
pas rétroactivement les compteurs déjà perdus. Le cycle natif corrigé de
|
||||
création et de rechargement est consigné dans les vérifications de l'alpha.25.
|
||||
|
||||
Avec **Générer les structures** désactivé, aucun monument n'est imposé ni
|
||||
enregistré. Ce changement ne modifie pas les océans et admissions des mondes23
|
||||
ou24. Le manifeste aérien25 réside dans `data/sanctuary-aerial-v25/` et son
|
||||
journal dans `data/sanctuary-world-v25/` ; les chemins historiques restent lus
|
||||
selon leur version.
|
||||
|
||||
## Limites et vérifications
|
||||
|
||||
Ce document décrit le contrat présent dans les sources, pas une annonce de
|
||||
publication. Les tests ajoutés couvrent les plans déterministes, la géologie
|
||||
relative, le rendu minéral par chunks, les réservations d'eau, le codec du
|
||||
monument et les journaux historiques. Les scénarios natifs de terrain, de
|
||||
structures conditionnelles et de cycle de vie du monument sont distincts des
|
||||
tests purs. Les résultats, graines, durées et réouvertures effectivement
|
||||
exécutés doivent être consignés dans le compte rendu de validation avant
|
||||
livraison ; aucune matrice native réussie n'est présumée ici.
|
||||
|
||||
Cette passe ne fournit pas de boss Sanctuary, de combat de progression, de
|
||||
portail Cavernes fonctionnel, de nouvelle dimension ni de raid. Les grands
|
||||
espaces explorables et le cadre central préparent ces usages sans les activer.
|
||||
@@ -0,0 +1,258 @@
|
||||
# Performances alpha.25 — mesures disponibles
|
||||
|
||||
État du relevé : **11 septembre 2026**. Les **neuf créations FULL** de la
|
||||
matrice 512 / 724 / 1024 × graines 0 / 42 / 2026 ont réussi après les marais 3D.
|
||||
Les mesures de ces créations sont présentées en premier ; les anciens témoins
|
||||
et l'hydrologie antérieure sont identifiés séparément. L'ordre inversé et les
|
||||
deux OFF complètent maintenant les **12/12 scénarios terrain** ; l'hydrologie
|
||||
finale à 1 800 ticks passe aussi. **Les benchmarks ont été arrêtés à la demande
|
||||
du joueur, qui souhaite tester le pack.** Ces mesures sont informatives et
|
||||
ne bloquent pas la fabrication ; aucune comparaison finale n'est requise
|
||||
ni promise. Voir le [reçu d'arrêt](../build/alpha25-evidence/performance-stopped-user-request.json).
|
||||
Ce relevé n'annonce ni la fin du build ni un pack.
|
||||
|
||||
Les essais ont été réalisés sur **Mac**, avec le serveur Fabric GameTest,
|
||||
Minecraft **26.3-pre-2** et Java 25, sans client graphique. Ils ne mesurent
|
||||
ni les FPS, ni les shaders, ni les performances sous Windows. La baseline
|
||||
alpha.24 ne conserve pas les conditions détaillées de matériel, de heap
|
||||
et de caches pour chaque scénario.
|
||||
|
||||
## Neuf créations FULL après les marais 3D
|
||||
|
||||
Les neuf cas passent sur la même empreinte de sources
|
||||
`4b0d9eb4456dc26ba75686322040eef02187b253db52277d5999a9b6eca36c8d`.
|
||||
Les [reçus synthétisés](../build/alpha25-evidence/matrix-final-summary.json)
|
||||
référencent pour chacun le rapport, le log et leurs empreintes. Le manifeste
|
||||
comprend douze scénarios : **9/9 créations et leurs trois contrôles
|
||||
complémentaires passent, soit 12/12**. Les contrôles de l'ordre inversé et des
|
||||
OFF ne sont pas présentés comme neuf mesures supplémentaires de création.
|
||||
|
||||
| Taille / graine | Appels FULL | Dans les appels FULL (s) | Boucle jusqu'aux entités prêtes (s) | Attente entités (ticks) | Heap après FULL (Mio) | Invocation complète (s) |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 512 / 0 | 133 | 49,888 | 64,552 | 396 | 533,0 | 136,525 |
|
||||
| 512 / 42 | 132 | 31,515 | 43,176 | 237 | 427,4 | 108,908 |
|
||||
| 512 / 2026 | 115 | 31,427 | 45,224 | 319 | 437,8 | 110,286 |
|
||||
| 724 / 0 | 138 | 80,599 | 109,251 | 803 | 836,8 | 210,333 |
|
||||
| 724 / 42 | 129 | 44,222 | 63,178 | 465 | 438,3 | 152,651 |
|
||||
| 724 / 2026 | 116 | 74,249 | 106,925 | 963 | 458,2 | 197,658 |
|
||||
| 1024 / 0 | 143 | 105,473 | 141,287 | 1163 | 855,1 | 278,374 |
|
||||
| 1024 / 42 | 152 | 75,549 | 104,950 | 939 | 850,0 | 249,329 |
|
||||
| 1024 / 2026 † | 141 | 123,031 | 161,201 | 1084 | 978,2 | 306,724 |
|
||||
|
||||
Les appels FULL portent sur des empreintes ciblées différentes, de 115 à 152 chunks,
|
||||
avec satellites et cavités ; ils ne génèrent pas l'intégralité de chaque île.
|
||||
`full_get_chunk_ms` inclut l'attente interne des dépendances ;
|
||||
`full_loop_elapsed_ms` ajoute notamment la cadence du harness et l'attente
|
||||
des entités. Leur différence n'est pas un chronomètre isolé de cette attente.
|
||||
L'invocation complète inclut Gradle, JVM, préparation et assertions. Les
|
||||
rapports n'isolent pas un temps total autonome de planification : les anciens
|
||||
diagnostics analytiques plus bas conservent leur périmètre propre.
|
||||
|
||||
**† Concurrence possible :** le dernier Grand 2026 a pu partager la machine
|
||||
avec des serveurs de vérification utilisant des sources et mondes isolés.
|
||||
L'ordre inversé et les deux OFF suivants peuvent aussi être concernés, selon
|
||||
[le relevé de concurrence](../build/alpha25-evidence/native-concurrency.json).
|
||||
Aucune de ces durées ne constitue une comparaison contrôlée ; ne pas attribuer
|
||||
un écart au diamètre ou à une modification du code sur cette seule mesure.
|
||||
Une comparaison contrôlée demanderait une machine sans autre serveur natif
|
||||
concurrent ; elle n'est pas poursuivie pour cette livraison.
|
||||
|
||||
Ces créations relèvent également l'ordonnanceur vanilla de fluides :
|
||||
|
||||
| Taille / graine | Mises à jour de fluide exécutées | Temps écoulé cumulé (ms) | Maximum d'un appel (ms) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 512 / 0 | 18 | 2,681 | 0,262 |
|
||||
| 512 / 42 | 88 | 28,087 | 13,281 |
|
||||
| 512 / 2026 | 2 | 8,157 | 6,964 |
|
||||
| 724 / 0 | 5 | 13,252 | 7,031 |
|
||||
| 724 / 42 | 19 | 3,376 | 0,279 |
|
||||
| 724 / 2026 | 36 | 27,271 | 10,660 |
|
||||
| 1024 / 0 | 5 | 4,709 | 0,736 |
|
||||
| 1024 / 42 | 17 | 4,734 | 0,371 |
|
||||
| 1024 / 2026 † | 3 | 42,271 | 27,550 |
|
||||
|
||||
Ce sont des snapshots du serveur de test depuis le début de leur instrumentation,
|
||||
pas les différences d'une fenêtre hydrologique fixe. Certains fluides hors des
|
||||
zones activées ne tournent pas ; ces faibles comptes ne prouvent ni l'absence
|
||||
de cascades ni le coût des bassins entièrement simulés. Ne pas comparer ces
|
||||
snapshots aux 600 ticks des témoins hydrologiques ci-dessous comme une charge
|
||||
équivalente. Les chronomètres utilisent du temps écoulé, jamais du temps CPU.
|
||||
Les pics des pools mémoire restent disponibles dans chaque `create.json` ;
|
||||
ils ne sont ni simultanés ni additionnables en pic global.
|
||||
|
||||
L'ordre inversé 512/0 demande 133 chunks, avec 82,534 s dans les appels FULL et
|
||||
102,518 s pour la boucle ; son invocation dure 216,802 s. Ces valeurs ne sont
|
||||
pas comparées en pourcentage à l'ordre normal : concurrence possible,
|
||||
ordonnancement et différences réelles de certains blocs de terrain rendent
|
||||
une attribution causale injustifiée. La [comparaison fonctionnelle](../build/alpha25-evidence/reverse-comparison-final.json)
|
||||
se limite aux plans, satellites, pièces et observations contrôlées ; elle
|
||||
signale explicitement les différences de comptage dans `terrain_blocks`.
|
||||
|
||||
## Hydrologie finale après les marais — observation de 1 800 ticks
|
||||
|
||||
Le [témoin 512/0](../build/alpha25-evidence/hydrology-512-0-wetlands-long-final1/hydrology25/512-0/full.json)
|
||||
passe sur la même source que les neuf créations. Les 176 appels FULL cumulent
|
||||
**105,781 s** ; la boucle jusqu'aux zones activées dure **173,686 s**, avec 1 712 ticks
|
||||
d'attente. Le scénario avec 1 800 ticks d'observation dure **227,718 s** ;
|
||||
l'invocation complète dure 359,184 s. Le heap final occupé vaut 661,1 Mio.
|
||||
|
||||
La différence des snapshots contient **20 498 mises à jour de fluides** et
|
||||
**1 071,803 ms** écoulées dans l'ordonnanceur vanilla, maximum 35,581 ms pour un
|
||||
appel. Cette fenêtre inclut chargement, attente et observation. Un trajet
|
||||
connecté atteint Y0, l'autre est observé sur le flanc à Y199 ; les surfaces
|
||||
des bassins restent complètes. Le détail fonctionnel et les bornes de lecture
|
||||
sont dans [le compte rendu](testing-alpha25.md).
|
||||
|
||||
Ce serveur pouvait tourner en parallèle des scénarios terrain dans une copie
|
||||
isolée. Les différences de temps face à l'ancien run de 600 ticks ne mesurent
|
||||
donc ni un coût propre aux marais ni une régression de génération. Les deux
|
||||
autres scénarios hydrologiques ci-dessous conservent leur date et leur scope
|
||||
antérieurs ; ils ne sont pas remplacés par cette seule observation prolongée.
|
||||
|
||||
## Comparaison antérieure avec l'alpha.24
|
||||
|
||||
Les valeurs 25 de ce tableau précèdent les marais 3D ; elles ne remplacent pas
|
||||
les neuf créations plus récentes.
|
||||
|
||||
**Aucun gain global alpha.24 → alpha.25 n'est établi.** Les empreintes de
|
||||
chunks, le terrain, les recherches et les étapes instrumentées diffèrent.
|
||||
Une durée absente signifie « non mesurée », jamais zéro.
|
||||
|
||||
| Mesure | Alpha.24 | Alpha.25 intermédiaire |
|
||||
|---|---:|---:|
|
||||
| Hydrologie région `(0,0)`, diamètre 512, graine 0 | 14,150 s ; 9 421 064 sondes | 9,017 s ; 5 015 684 sondes |
|
||||
| Création FULL, 512 / 0 | 57 chunks ; durée FULL non mesurée | 133 chunks ; appels `getChunk` 48,017 s ; boucle jusqu'aux entités prêtes 64,657 s |
|
||||
| Attente des entités, 512 / 0 | 127 ticks | 385 ticks |
|
||||
| Création FULL, 512 / 42 | 61 chunks ; durée FULL non mesurée | 132 chunks ; appels 30,080 s ; boucle 39,779 s ; attente 192 ticks |
|
||||
| Deux réouvertures alpha.25, 512 / 0 | Pas de témoin équivalent instrumenté | 133 chunks ; appels 0,601 / 0,593 s ; boucle 5,126 / 5,008 s ; attente 4 / 5 ticks |
|
||||
| Heap occupé après FULL, créations 512 / 0 et 42 | Non mesuré | 621,5 / 445,8 Mio |
|
||||
| Diagnostic analytique de neuf contextes 25 | Pas de mesure équivalente | 8,961–39,374 s par contexte, préparation et assertions incluses |
|
||||
|
||||
Les compteurs de planification hydrologique régionale correspondent au
|
||||
même périmètre instrumental, mais les formes et résultats recherchés
|
||||
changent : leur différence ne prouve pas une optimisation à résultat
|
||||
constant. Les chronomètres des autres planificateurs peuvent contenir des
|
||||
appels hydrologiques imbriqués ; ils ne doivent pas être additionnés pour
|
||||
reconstituer une durée totale.
|
||||
|
||||
L'alpha.24 conserve aussi des durées d'invocation complètes. Par exemple,
|
||||
le scénario 512 / 0 dure 62,640 s, dont 10,840 s pour sa suite native.
|
||||
Ces durées incluent des périmètres différents de la boucle FULL 25 :
|
||||
Gradle, démarrage de la JVM et du serveur, préparation du monde et
|
||||
assertions. Les diviser par le nombre de chunks ne donne pas un coût de
|
||||
génération par chunk.
|
||||
|
||||
Sources : [baseline alpha.24](../build/alpha25-evidence/performance-baseline24.json),
|
||||
[neuf plans25](../build/alpha25-evidence/diagnostics-final1/plans.json),
|
||||
[terrain512/0](../build/alpha25-evidence/terrain-512-0-create-final1/create.json),
|
||||
[terrain512/42](../build/alpha25-evidence/terrain-512-42-create-final1/create.json),
|
||||
[réouverture1](../build/alpha25-evidence/terrain-512-0-reload1-final1/reload1.json)
|
||||
et [réouverture2](../build/alpha25-evidence/terrain-512-0-reload2-final1/reload2.json).
|
||||
|
||||
## Fluides et attente des voisins — avant les marais 3D
|
||||
|
||||
| Hydrologie physique 25 | Chunks FULL | Appels `getChunk` | Jusqu'à simulation prête | Attente | Observation | Mises à jour de fluide | Temps cumulé du planificateur |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 512 / 0, deux sorties | 176 | 64,337 s | 93,626 s | 880 ticks | 600 ticks | 14 582 | 262,613 ms |
|
||||
| 512 / 42, zéro sortie | 99 | 31,511 s | 59,161 s | 899 ticks | 600 ticks | 3 213 | 149,320 ms |
|
||||
| 1 024 / 2026, une sortie | 247 | 135,398 s | 286,287 s | 5 226 ticks | 600 ticks | 24 200 | 451,869 ms |
|
||||
|
||||
`full_get_chunk_ms` cumule le temps écoulé dans les appels explicites
|
||||
`getChunk(FULL)`. Ces appels peuvent eux-mêmes attendre leurs dépendances.
|
||||
`full_loop_elapsed_ms` inclut ensuite l'attente des voisins et des entités,
|
||||
ainsi que la cadence du harness. La différence entre ces deux valeurs
|
||||
n'est donc pas un chronomètre isolé d'attente. Les ticks d'attente ne
|
||||
doivent pas être convertis en secondes en supposant systématiquement
|
||||
20 ticks par seconde : la cadence des serveurs de test est configurée.
|
||||
|
||||
Le temps du planificateur de fluides utilise `System.nanoTime()` autour de
|
||||
l'appel vanilla : c'est un **temps écoulé instrumenté**, pas une mesure
|
||||
de temps CPU. Les valeurs du tableau sont les différences entre les deux
|
||||
snapshots. Leur intervalle inclut le chargement et l'attente, puis les
|
||||
600 ticks d'observation. Le nombre de ticks de jeu est distinct du nombre
|
||||
de mises à jour individuelles de fluides exécutées.
|
||||
|
||||
Le premier témoin 512/0 utilisait une baseline filtrée sur les fluides
|
||||
présents. Un [recomptage indépendant](../build/alpha25-evidence/hydrology-512-0-attempt1/waterline-completeness.json)
|
||||
a confirmé que les 3 343 / 1 357 / 1 195 cellules prévues étaient toutes
|
||||
présentes avant et après observation. Il s'agit d'une preuve complémentaire
|
||||
sur ce run, pas d'une réexécution de la fixture corrigée.
|
||||
|
||||
L'alpha.24 vérifiait une cascade d'îlot jusqu'à Y8 après 1 180 ticks
|
||||
d'attente et une retenue générale pendant 1 800 ticks. Elle ne mesurait
|
||||
ni le nombre de mises à jour de fluides ni leur temps cumulé. Ces durées
|
||||
d'observation ne sont pas comparables aux compteurs supplémentaires 25.
|
||||
|
||||
Sources : [hydro512/0](../build/alpha25-evidence/hydrology-512-0-attempt1/full.json),
|
||||
[hydro512/42](../build/alpha25-evidence/hydrology-512-42-final1/512-42/full.json)
|
||||
et [hydro1024/2026](../build/alpha25-evidence/hydrology-1024-2026-final1/1024-2026/full.json).
|
||||
|
||||
## Mémoire, sondages exacts et limites
|
||||
|
||||
La mémoire occupée après FULL est un instantané du heap Java, pas la
|
||||
mémoire résidente du processus. Les rapports 25 conservent également les
|
||||
pics des différents pools mémoire. Ces pics peuvent survenir à des
|
||||
instants différents : **ne pas les sommer en pic global**. L'alpha.24 ne
|
||||
conserve pas de mesure correspondante.
|
||||
|
||||
Une optimisation locale 25 possède une comparaison exacte à résultat
|
||||
identique, sur 864 colonnes natives et 2 592 recherches de plafonds :
|
||||
|
||||
- scans de roche naturelle : **253 651 → 197 616 sondes**, soit −22,09 % ;
|
||||
- plafonds de bosquets : **223 403 → 6 501 sondes**, soit −97,09 %.
|
||||
|
||||
Ces chiffres comparent les anciens et nouveaux parcours de sondage sur
|
||||
le même terrain 25. Ils ne constituent ni une comparaison alpha.24/25,
|
||||
ni une réduction démontrée du temps de chargement global.
|
||||
|
||||
Les témoins spécialisés des marais, avec huit à treize chunks demandés,
|
||||
contrôlent sols, biomes, plantes et options. Leurs invocations ne constituent
|
||||
pas un benchmark apparié et ne remplacent pas les chronomètres de terrain.
|
||||
|
||||
La comparaison 24/25 prévue sur **289 + 80 chunks fixes**, même taille 724 et
|
||||
graine 0, a été interrompue avant une paire de résultats complète. Aucun gain
|
||||
ni coût relatif n'en est déduit. Les critères de heap, ordre des chunks, état
|
||||
neuf et cadence identiques restent une limite méthodologique, pas un chantier
|
||||
supplémentaire promis. Aucun résultat Windows ou de rendu client n'est
|
||||
disponible dans ce relevé.
|
||||
|
||||
## Reproduire les chronomètres existants
|
||||
|
||||
Le harness natif existant `Terrain25WorldGameTests` mesure les appels
|
||||
FULL, la boucle jusqu'aux entités prêtes, les attentes, le heap et les
|
||||
pools, puis fournit un snapshot du planificateur de fluides. Il travaille
|
||||
dans les mondes de développement jetables du serveur GameTest, jamais
|
||||
dans une sauvegarde Prism personnelle.
|
||||
|
||||
Depuis la racine du dépôt :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryTerrain25Tests=true \
|
||||
-PsanctuaryTestDiameter=512 \
|
||||
-PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryTerrain25Phase=create
|
||||
```
|
||||
|
||||
Pour relire le même monde de test après cette création :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryTerrain25Tests=true \
|
||||
-PsanctuaryTestDiameter=512 \
|
||||
-PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryExpansionReload=true \
|
||||
-PsanctuaryTerrain25Phase=reload1
|
||||
```
|
||||
|
||||
La seconde réouverture utilise `-PsanctuaryTerrain25Phase=reload2`.
|
||||
Conserver le rapport JSON et le journal du run avec la révision exacte
|
||||
des sources. Rejouer ces commandes sur les sources finales produira de
|
||||
nouvelles mesures ; cela ne rendra pas rétrospectivement identiques les
|
||||
conditions des témoins intermédiaires. Aucun nouveau benchmark n'est
|
||||
introduit par ce document.
|
||||
|
||||
Les liens vers `build/` désignent des preuves locales ignorées par Git ;
|
||||
ils ne constituent pas des artefacts publiés du dépôt.
|
||||
@@ -0,0 +1,465 @@
|
||||
# Alpha.25 — état des vérifications
|
||||
|
||||
Le chantier `codex/terrain-sanctuary-alpha25` est **construit et vérifié localement**.
|
||||
Les essais ci-dessous utilisent Mac Apple Silicon, Java 25 et Minecraft
|
||||
26.3-pre-2. **Les douze scénarios de terrain passent : neuf créations,
|
||||
un ordre inversé et deux OFF.** `check build` et l'assemblage du pack passent ;
|
||||
le MRpack et le bootstrap Prism sont vérifiés. La publication et la
|
||||
synchronisation Prism sont encore en cours.
|
||||
L’alpha.24 reste le canal stable publié. Aucun essai Windows ni contrôle visuel
|
||||
complet du client n’est revendiqué.
|
||||
|
||||
Le joueur souhaite désormais **tester le pack sans attendre les performances**.
|
||||
Les [benchmarks sont arrêtés volontairement](../build/alpha25-evidence/performance-stopped-user-request.json) ;
|
||||
les mesures conservées sont non bloquantes. Un benchmark est prévu **après
|
||||
la livraison**, pour orienter les optimisations de l'alpha.26 ; aucune
|
||||
optimisation de production supplémentaire n'est ajoutée à l'alpha.25.
|
||||
|
||||
La réception demandée concerne désormais les **nouveaux mondes de test**.
|
||||
La compatibilité des anciennes sauvegardes n’est plus un critère de livraison.
|
||||
Les anciens témoins déjà exécutés restent des preuves distinctes ; aucun monde
|
||||
personnel n’est migré ou régénéré.
|
||||
|
||||
## État actuel
|
||||
|
||||
| Périmètre | Preuve acquise | Limite de cette preuve |
|
||||
| --- | --- | --- |
|
||||
| Marais de surface et souterrains | FULL 512/0 et 512/42, sols, biomes 3D et flore native | Deux graines, pas toute la matrice des tailles ; sources testées distinctes |
|
||||
| Options des marais | ON, Structures Minecraft OFF et Expérimentales OFF passent pour 512/42 | Contrôle du système de marais, pas de toutes les structures du monde |
|
||||
| Monuments océaniques | Six créations et six réouvertures, gardiens et immersion | Un seul océan préparé entièrement jusqu’à READY |
|
||||
| Terrain, satellites et sanctuaire | 12/12 : neuf créations FULL, ordre inversé et deux OFF | L'ordre inversé ne démontre pas une île entière identique |
|
||||
| Hydrologie | 512/0 final sous 1800 ticks : une chute à Y0, l'autre sur un flanc | Les témoins zéro/une sortie restent antérieurs aux marais finaux |
|
||||
| Structures conditionnelles | Village 512/18 et mine 724/42 : création et rechargement FULL ; hutte et salle à spawner vérifiées | Cabane du mineur sans témoin natif positif ; guérison complète d'un zombie-villageois non testée |
|
||||
| Distribution locale | `check build`, assemblage, MRpack et bootstrap vérifiés | Publication et synchronisation Prism encore en cours |
|
||||
|
||||
Les reçus locaux sont conservés sous `build/alpha25-evidence/`, dossier ignoré
|
||||
par Git. Un statut s’appuie sur le `run.json`, le rapport de la phase et le log
|
||||
correspondants : un ancien `failure.json` recopié dans un dossier n’annule ni
|
||||
ne valide une exécution plus récente. Les empreintes de sources des reçus ne
|
||||
sont pas celles d’un JAR livré. La présence d’un protocole dans le code ne
|
||||
constitue jamais une validation de son exécution.
|
||||
|
||||
## Terrain : neuf créations FULL réussies
|
||||
|
||||
Les **512 / 724 / 1024 × graines 0 / 42 / 2026** passent toutes sur la même
|
||||
empreinte de sources
|
||||
`4b0d9eb4456dc26ba75686322040eef02187b253db52277d5999a9b6eca36c8d`,
|
||||
après les marais de surface et inférieurs. La [synthèse des reçus](../build/alpha25-evidence/matrix-final-summary.json)
|
||||
contrôle pour chaque cas le rapport, le code de sortie et le log ; elle
|
||||
référence leurs empreintes. Le manifeste complet passe désormais **12/12** :
|
||||
**9/9 créations**, plus l'ordre inversé et les deux désactivations de structures.
|
||||
|
||||
| Taille / graine | Chunks FULL demandés | Chunks recensés pour le terrain | Mooshrooms observées | Escaliers du sanctuaire vérifiés |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 512 / 0 | 133 | 77 | 6 | 468 |
|
||||
| 512 / 42 | 132 | 71 | 3 | 479 |
|
||||
| 512 / 2026 | 115 | 71 | 6 | 450 |
|
||||
| 724 / 0 | 138 | 77 | 4 | 432 |
|
||||
| 724 / 42 | 129 | 72 | 3 | 482 |
|
||||
| 724 / 2026 | 116 | 79 | 6 | 499 |
|
||||
| 1024 / 0 | 143 | 92 | 6 | 465 |
|
||||
| 1024 / 42 | 152 | 94 | 3 | 459 |
|
||||
| 1024 / 2026 | 141 | 93 | 6 | 471 |
|
||||
|
||||
Chaque création observe cinq coffres de satellites, deux marchands et une
|
||||
source de cascade sur un seul îlot. Les dix-huit îlots ont entre 76,12 % et
|
||||
86,87 % de colonnes herbeuses, avec deux à quatre arbres chacun. Tous les
|
||||
sanctuaires minéraux possèdent leur vrai départ et leur pièce sauvegardée :
|
||||
28 blocs de cadre blackstone, 35 blocs d'ouverture d'air, 175 blocs d'accès
|
||||
dégagés et aucun bloc de portail Nether. Le comptage des minerais, végétaux
|
||||
et blocs naturels porte sur les chunks recensés, pas sur toute l'île.
|
||||
Les échantillons de cavités ne garantissent pas un lac d'eau et un lac de
|
||||
lave dans chaque monde : les refus naturels demeurent possibles.
|
||||
|
||||
Ces créations pillent des coffres et déplacent ou retirent des habitants dans
|
||||
leurs mondes jetables. Elles ne remplacent pas à elles seules une réouverture
|
||||
sur cette révision. Les anciens cycles de réouverture sont distingués plus bas.
|
||||
Des serveurs isolés peuvent avoir partagé la machine avec le dernier Grand 2026,
|
||||
puis l'ordre inversé et les OFF : voir [le relevé de concurrence](../build/alpha25-evidence/native-concurrency.json).
|
||||
Cela ne transforme pas leurs chronomètres en comparaison contrôlée ; le
|
||||
[bilan de performances](performance-alpha25.md) en précise les périmètres.
|
||||
|
||||
Le cas **Petit 512 / 0 en ordre inversé** passe aussi sur cette révision.
|
||||
La [comparaison](../build/alpha25-evidence/reverse-comparison-final.json)
|
||||
retrouve exactement géométrie des satellites, codec, pièces sauvegardées,
|
||||
blocs contrôlés avant/après, mutations, végétation des îlots et mooshrooms.
|
||||
**Elle ne démontre pas une île entière identique** : `terrain_blocks` diffère
|
||||
pour certaines décorations et roches, par exemple herbe courte 1773/1441,
|
||||
dripstone 15789/16432 et deepslate 450180/448994. Ces différences restent
|
||||
publiées dans le rapport ; le scope réussi n'est pas étendu à ces totaux.
|
||||
|
||||
Les deux OFF **Moyen 724 / 0** passent dans des JVM distinctes. Avec toutes
|
||||
les structures désactivées, plans aériens, secrets, rails, traversée et salle
|
||||
restent absents. Avec seulement les expérimentales désactivées, les plans
|
||||
optionnels restent absents, mais la traversée conserve ses 23 modules et la
|
||||
salle son réseau ; la génération vanilla reste autorisée. Chaque scénario
|
||||
inspecte trois chunks FULL naturels, conservant leurs biomes 25 et des roches
|
||||
au-dessus de Y250, sans start ni rail dans ces échantillons. L'autorisation
|
||||
des structures vanilla dans le second cas ne prouve donc pas leur présence.
|
||||
Preuves : [OFF général](../build/alpha25-evidence/terrain-724-0-global-off-final1/global-off.json)
|
||||
et [OFF expérimental](../build/alpha25-evidence/terrain-724-0-experimental-off-final1/experimental-off.json).
|
||||
|
||||
## Marais 3D : terrain réel et flore sous les voûtes
|
||||
|
||||
Les deux témoins passent chacun leurs **18 GameTests**. Les sites inférieurs
|
||||
référencés depuis plusieurs régions sont dédupliqués avant le comptage.
|
||||
|
||||
| Contrôle FULL | Petit 512 / graine 0 | Petit 512 / graine 42 |
|
||||
| --- | ---: | ---: |
|
||||
| Strates de surface / souterraines du plan | 0 / 2336 | 1853 / 1237 |
|
||||
| Niveaux d’eau des sites inférieurs | Y160 et Y170 | Y189 |
|
||||
| Chunks explicitement demandés | 8 | 13 |
|
||||
| Eau / berges de surface inspectées | 0 / 0 | 12 / 12 |
|
||||
| Eau / berges souterraines inspectées | 12 / 5 | 12 / 2 |
|
||||
| Paires sol–sous-sol exactement conformes | 17 | 38 |
|
||||
| Quarts de biomes de cave préservés hors enveloppes | 689 | 1254 |
|
||||
| Colonnes inférieures inspectées pour la flore | 579 | 323 |
|
||||
| Blocs de végétation inférieure, tous sous roche | 38 | 8 |
|
||||
| Hutte réelle avec sorcière et chat | Aucune admise | Une, en surface |
|
||||
|
||||
La graine 0 contient sous roche 12 nénuphars, 12 blocs de liane, 11 herbes courtes,
|
||||
2 herbes hautes et 1 canne à sucre. La graine 42 contient sous roche 1 buisson à
|
||||
lucioles, 4 nénuphars et 3 cannes à sucre ; ses 949 colonnes de surface inspectées
|
||||
contiennent 53 herbes courtes, 6 cannes, 195 blocs de liane, 6 nénuphars et 2 champignons
|
||||
bruns. Tous les végétaux observés satisfont leur condition native de survie.
|
||||
Il s’agit de blocs, pas de plantes indépendantes pour les végétaux multiblocs,
|
||||
ni d’un inventaire botanique de toute l’île.
|
||||
|
||||
Les enveloppes stockent réellement `sanctuary:terrain25_swamp` ; hors de leur
|
||||
union, les quarts de biome des colonnes témoins restent égaux à la référence.
|
||||
Les sols suivent l’eau préexistante, sans nouvelle nappe ou plateforme.
|
||||
Aucun minerai ne figure dans les 55 paires inspectées : la préservation des
|
||||
minerais est vérifiée séparément par le test sur états de blocs natifs, sans
|
||||
revendiquer ici un filon réel. Aucune des deux graines n’a deux marais au même
|
||||
X/Z : ce cas est couvert par une fixture pure à deux lacs superposés.
|
||||
|
||||
Preuves : [512/0 création](../build/alpha25-evidence/wetlands-lower-512-0-on-final3/create.json),
|
||||
[son reçu](../build/alpha25-evidence/wetlands-lower-512-0-on-final3/run.json),
|
||||
[512/42 création](../build/alpha25-evidence/wetlands-lower-512-42-on-final3/create.json)
|
||||
et [son reçu](../build/alpha25-evidence/wetlands-lower-512-42-on-final3/run.json).
|
||||
La graine 0 précède les deux gardes de flore ; la graine 42 les intègre
|
||||
(source `9dec8ea64c6107ed873d86f013da8e01c8315dfcc262e72ed689394c1a1f4aef`).
|
||||
Ses 77,723 s couvrent toute la commande ; les 10,745 s du rapport mesurent le
|
||||
scénario après sa préparation, pas la création complète de l’île.
|
||||
|
||||
Les régressions corrigées sont le tuff non revêtu, une liane native continuée
|
||||
après refus de son support et un champignon accepté avant éclairage définitif.
|
||||
Pour ce dernier, le monde sauvegardé montrait un champignon sur herbe avec
|
||||
SkyLight 13, trop lumineux selon vanilla, et une source de ciel 15 distante de
|
||||
deux blocs. La garde conservative conserve les champignons abrités et les
|
||||
substrats bénéficiant de l’exemption vanilla ; les deux champignons du
|
||||
nouveau témoin 42 restent valides. Voir [le relevé NBT](../build/alpha25-evidence/wetlands42-mushroom-light-audit.json).
|
||||
Les anciens échecs restent archivés et ne sont pas comptés comme succès.
|
||||
|
||||
Avec **Structures Minecraft OFF**, puis **Expérimentales OFF**, le témoin 42
|
||||
repasse dans deux nouvelles JVM sur les mêmes sources : 3090 strates, les
|
||||
38 paires de sol, 1254 quarts de cave et 8 blocs de flore inférieure sont
|
||||
conservés, sans hutte. Les trois cas totalisent **54 GameTests réussis**.
|
||||
Le SHA-256 du plan naturel vaut dans les trois cas
|
||||
`34335a9d8349c041201a3b5eef639a6e9be5ca838ebc6478296831a52c9a5ab6`.
|
||||
Preuves : [rapport OFF](../build/alpha25-evidence/wetlands-lower-512-42-global-off-final1/wetlands25/global-off.json)
|
||||
et [reçu](../build/alpha25-evidence/wetlands-lower-512-42-global-off-final1/run.json),
|
||||
[rapport Expérimentales OFF](../build/alpha25-evidence/wetlands-lower-512-42-experimental-off-final1/wetlands25/experimental-off.json)
|
||||
et [reçu](../build/alpha25-evidence/wetlands-lower-512-42-experimental-off-final1/run.json).
|
||||
Ces résultats remplacent, pour le système de marais, les premiers témoins
|
||||
ON/OFF qui ne couvraient encore que la surface.
|
||||
La [comparaison des trois plans](../build/alpha25-evidence/wetlands-structure-options-comparison.json)
|
||||
consigne leurs empreintes et leurs nombres de strates.
|
||||
|
||||
Un passage global OFF supplémentaire valide aussi la sélection locale de
|
||||
hauteur des huttes avec les vrais tags natifs chargés : une colonne témoin
|
||||
renvoie Y190 au-dessus de l'eau, puis Y191 sur une berge de pierre, sans
|
||||
effacer son plafond supérieur. Les 18 GameTests et le marais réel passent
|
||||
sur la source `36ebe0db5a294ed64b924d9952c828f76719cf200925849abc72a0ce5448a17f`
|
||||
([reçu](../build/alpha25-evidence/wetlands-lower-512-42-global-off-heightmap-final2/run.json)).
|
||||
La colonne de ce contrôle d'altitude est construite par le test : elle ne
|
||||
prouve pas la présence d'une hutte souterraine naturellement générée.
|
||||
|
||||
## Monuments : six climats/tailles, douze phases natives
|
||||
|
||||
Les **douze phases** passent dans des JVM distinctes, avec serveur de graine 0,
|
||||
île initiale 724, structures Minecraft ON et expérimentales OFF. Chaque création
|
||||
utilise la vraie admission `Session.create` et capture le départ vanilla, ses
|
||||
pièces et son emprise avant la réservation. Le premier candidat est accepté
|
||||
pour chacune des six combinaisons.
|
||||
|
||||
| Océan | Admission | Périmètre atteint | Scénario création / rechargement |
|
||||
| --- | ---: | --- | ---: |
|
||||
| 512 glacial | 466 ms | Réservé, monument FULL | 28,06 / 3,21 s |
|
||||
| 512 tempéré | 534 ms | **READY réel**, 1326 chunks | 275,87 / 2,92 s |
|
||||
| 512 chaud | 471 ms | Réservé, monument FULL | 29,04 / 2,99 s |
|
||||
| 1024 glacial | 563 ms | Réservé, monument FULL | 33,01 / 3,05 s |
|
||||
| 1024 tempéré | 618 ms | Réservé, monument FULL | 31,16 / 2,97 s |
|
||||
| 1024 chaud | 599 ms | Réservé, monument FULL | 31,30 / 3,53 s |
|
||||
|
||||
Chaque phase demande explicitement 37 chunks couvrant le monument, ses voisins
|
||||
et le lieu d’extraction du gardien. Chaque création observe trois grands
|
||||
gardiens ; les réouvertures conservent un gardien tué absent, un gardien déplacé
|
||||
hors du monument et le troisième sur place. Un bloc de prismarine démoli
|
||||
reste absent, sans duplication ni reconstruction. Les monuments 512 contiennent
|
||||
initialement 13242 blocs de prismarine et 128 lanternes ; les 1024 en contiennent
|
||||
12847 et 114. Chacun contient huit blocs d’or.
|
||||
|
||||
Les 207 positions inspectées au-dessus et autour du monument contiennent de
|
||||
l’eau réelle, hors des portes et pièces intérieures. Le test autorise aussi la
|
||||
glace native, mais ces témoins n’en prouvent pas la présence. Chaque phase
|
||||
obtient entre un et quatre gardiens ordinaires via `NaturalSpawner`, avec eau,
|
||||
table native, collisions et distance au joueur. Ce contrôle ne mesure ni le
|
||||
plafond global de mobs ni le débit d’une ferme.
|
||||
|
||||
L’océan tempéré 512 atteint réellement READY après 1326 chunks préparés ; sa file
|
||||
est inactive et le journal confirme cet état. L’attente réelle de préparation
|
||||
vaut 248,21 s après le chargement local du monument. Le tag vanilla des cartes
|
||||
océaniques trouve le départ enregistré, sans créer d’île ni modifier le
|
||||
journal. Une seconde recherche exclut ce départ déjà référencé. Le compteur 1
|
||||
persiste dans une autre JVM : le finder marque désormais le chunk gagnant à
|
||||
sauvegarder après `addReference`. Ce correctif ne transforme pas la référence
|
||||
cartographique en visite de joueur. Les cinq autres océans, restés réservés,
|
||||
sont correctement exclus du locator ; ils ne prouvent ni READY ni localisation
|
||||
positive.
|
||||
|
||||
Preuves : [tempéré 512 création](../build/alpha25-evidence/monument-512-temperate-create-final4/create.json)
|
||||
et [rechargement](../build/alpha25-evidence/monument-512-temperate-reload-final4/reload.json).
|
||||
Les cinq autres paires sont sous
|
||||
`build/alpha25-evidence/monument-<taille>-<climat>-<create|reload>-final1/`
|
||||
(climats `glacial`, `temperate`, `warm`). Leurs douze `run.json` ont exit 0 et
|
||||
leurs logs confirment 18 GameTests chacun, sur l’empreinte de sources commune
|
||||
`75b3ed4eab89245acf8f9fc6860d59dfdb14571d14731f2c5d8e95ffd81e5ca7`.
|
||||
Il s’agit d’une graine de serveur et d’expansions neuves ; l’admission dans du
|
||||
vide déjà exploré avec certificat reste un contrôle distinct à recevoir.
|
||||
|
||||
Le [scénario océan général OFF](../build/alpha25-evidence/monument-global-off-final1/global-off.json)
|
||||
passe aussi sur la source de la matrice de terrain. Une vraie réservation
|
||||
d'océan 512 tempéré est acceptée sans plan de monument ni certificat ;
|
||||
12 candidats natifs sont contrôlés, deux chunks FULL conservent 23664 blocs
|
||||
d'eau et aucun monument. Les recherches restent vides, sans changer le
|
||||
journal. Ce cas s'arrête à l'état réservé, sans prétendre atteindre READY.
|
||||
|
||||
## Hydrologie finale : une chute au vide, une sur le flanc
|
||||
|
||||
Le [témoin Petit 512 / 0](../build/alpha25-evidence/hydrology-512-0-wetlands-long-final1/hydrology25/512-0/full.json)
|
||||
passe après les marais finaux, sur la même source `4b0d9eb4…` que les neuf
|
||||
créations. Après activation des 176 chunks demandés puis **1800 ticks
|
||||
d'observation native**, les surfaces d'eau **3343 et 1357 blocs** et de lave
|
||||
**1195 blocs** restent complètes, avec leurs appuis ; les 64/51/64 berges
|
||||
échantillonnées restent sèches et aucun contact eau/lave final n'est détecté
|
||||
dans les volumes inspectés.
|
||||
|
||||
La lecture finale, sans l'ancien arrêt après 16 blocs de descente, trouve :
|
||||
|
||||
| Sortie de rivière | Plus basse eau connectée observée | Résultat |
|
||||
| --- | --- | --- |
|
||||
| Lèvre près de -147 243 0 | -157 **0** 5 | L'eau atteint réellement Y0 |
|
||||
| Lèvre près de 19 243 -79 | 24 **199** -72 | L'eau rejoint un flanc naturel |
|
||||
|
||||
Les recherches n'atteignent pas leur borne de 120000 cellules. Elles restent
|
||||
limitées à l'emprise de chaque sortie +7 blocs, avec un parcours horizontal
|
||||
et descendant. Le second résultat ne signifie ni arrêt permanent de l'eau,
|
||||
ni absence d'un autre trajet plus loin. Les 20498 mises à jour de fluides
|
||||
proviennent du scheduler vanilla, sans préremplissage ni propagation manuelle.
|
||||
Le contact eau/lave final ne retrace pas les réactions éventuellement déjà
|
||||
terminées. Ce run ne remplace pas les témoins zéro/une sortie ni un essai visuel.
|
||||
Voir [le reçu](../build/alpha25-evidence/hydrology-512-0-wetlands-long-final1/run.json)
|
||||
et le [périmètre des chronomètres](performance-alpha25.md).
|
||||
|
||||
## Terrain et hydrologie : preuves antérieures distinctes
|
||||
|
||||
La [matrice des neuf plans](../build/alpha25-evidence/diagnostics-final1/plans.json)
|
||||
512/724/1024 × graines 0/42/2026 passe après correction des membranes, sols DRY
|
||||
et bornes de sondage : 12 lacs d’eau, 11 de lave et 7 sorties de rivière admis.
|
||||
Ces plans ne sont pas des chunks FULL. Les sommets du relevé atteignent Y349 ;
|
||||
Y358 reste un plafond, pas une hauteur imposée.
|
||||
|
||||
Les créations FULL 512/0 et 512/42 passent ; 512/0 passe aussi deux réouvertures.
|
||||
Le premier témoin observe cinq satellites, deux marchands, six mooshrooms,
|
||||
champignons géants, minerais profonds et lacs. Coffres pillés, extraction des
|
||||
habitants et démolitions restent conservés après rechargement. Son sanctuaire
|
||||
minéral contient 28 blocs de cadre blackstone, 35 blocs d’ouverture d’air,
|
||||
175 blocs d’accès libres et 468 escaliers avec orientations/appuis vérifiés,
|
||||
sans surface de portail Nether. Les 133 appels FULL cumulent 48,02 s ; 64,66 s
|
||||
incluent la boucle et l’attente des entités. Preuves sous
|
||||
`build/alpha25-evidence/terrain-512-0-<create|reload1|reload2>-final1/`
|
||||
et `terrain-512-42-create-final1/`.
|
||||
|
||||
Les trois témoins hydrologiques passent sous **600 ticks natifs** après activation :
|
||||
|
||||
| Taille / graine | Cascades | Chunks FULL demandés | Surfaces eau / lave retenues | Ticks de fluides exécutés |
|
||||
| --- | ---: | ---: | --- | ---: |
|
||||
| 512 / 0 | 2 | 176 | 3343+1357 / 1195 | 14582 |
|
||||
| 512 / 42 | 0 | 99 | 2414+1336 / aucune lave admise | 3213 |
|
||||
| 1024 / 2026 | 1 | 247 | 1128 / 1018 | 24200 |
|
||||
|
||||
Les surfaces prévues restent présentes, leurs deux couches d’appui continues,
|
||||
et les berges échantillonnées sèches. Les écoulements sont connectés et
|
||||
descendent d’au moins 16 blocs ; cela ne prouve pas une chute jusqu’à Y0. Aucun
|
||||
fluide n’est prérempli et aucun tick ajouté hors de l’ordonnanceur vanilla.
|
||||
La comparaison eau/lave porte sur l’état final, pas l’historique de toutes
|
||||
les réactions. Le premier témoin 512/0 a reçu une preuve complémentaire de
|
||||
complétude des surfaces, après audit de la capture initialement filtrée :
|
||||
`hydrology-512-0-attempt1/waterline-completeness.json`. Les deux autres utilisent
|
||||
directement la capture complète corrigée. Rapports sous
|
||||
`hydrology-512-0-attempt1/`, `hydrology-512-42-final1/` et
|
||||
`hydrology-1024-2026-final1/`.
|
||||
|
||||
Les durées des appels FULL, de l’attente des voisins/entités, de l’ordonnanceur
|
||||
de fluides et la mémoire sont séparées dans le [bilan de performances](performance-alpha25.md).
|
||||
Ni ces durées ni les mesures intermédiaires ne démontrent un gain global de
|
||||
chargement face à 24 ou le temps de lancement d’un client.
|
||||
|
||||
## Structures conditionnelles et contrôles restant ouverts
|
||||
|
||||
Le donjon vanilla à **-63 204 63** et la hutte à **-44 246 -125** sont vérifiés
|
||||
en FULL 512/42 puis réouverts dans une autre JVM : un spawner réel, le départ
|
||||
natif de la hutte et ses indicateurs de placement des habitants persistent.
|
||||
Ce cycle, antérieur aux marais inférieurs, ne simule pas la mort ou l’extraction
|
||||
de la sorcière et du chat. Preuves sous `underground-512-42-<create|reload>-final1/`.
|
||||
La nouvelle création des marais 42 confirme à nouveau la sorcière et le chat
|
||||
vivants, avec les quatre appuis sur leur sol naturel.
|
||||
|
||||
Les quatre phases dédiées **village 512/18 et mine 724/42, création puis
|
||||
réouverture dans des JVM distinctes**, passent : voir la
|
||||
[synthèse](../build/alpha25-evidence/isolated-underground-required-summary.json).
|
||||
Elles utilisent une copie isolée, source
|
||||
`1f46e5cd111a91bc80c451b787aec8d0d1c437f50ad049007e7ab0c624c6c463`,
|
||||
avec uniquement la normalisation native du comparateur NBT ajustée.
|
||||
|
||||
Le village de **Petit 512 / graine 18**, repéré à **-52 148 -5**, est désormais
|
||||
vérifié sur 121 chunks FULL : 90 pièces Jigsaw, dont 14 maisons, et sept
|
||||
zombie-villageois vanilla persistants. Les sept UUID sont conservés après
|
||||
extraction d'un occupant, pillage d'un coffre et démolition d'un bloc de rue ;
|
||||
le coffre reste vide et le bloc absent. Le premier décodage normalise les
|
||||
28 emprises Jigsaw natives agrandies vers les dimensions exactes des templates ;
|
||||
tous les autres champs, puis le second décodage, sont comparés strictement.
|
||||
Le test vérifie le type natif guérissable, sans effectuer une guérison complète.
|
||||
Preuves : [création](../build/alpha25-evidence/underground-512-18-village-create-required-nbt2/create.json)
|
||||
et [rechargement](../build/alpha25-evidence/underground-512-18-village-reload-required-nbt2/reload.json).
|
||||
|
||||
La mine de **Moyen 724 / graine 42**, entrée **6 190 6**, possède 83 pièces
|
||||
natives et est inspectée sur 33 chunks FULL. Les 204 rails, 187 toiles,
|
||||
632 barrières et 895 planches observés restent identiques au rechargement.
|
||||
Cinq wagonnets-coffres conservent leurs UUID, tables de butin et graines ;
|
||||
deux spawners d'araignées venimeuses conservent leur configuration vanilla.
|
||||
Ce cycle ne simule ni le pillage des wagonnets ni l'activation des spawners.
|
||||
Preuves : [création](../build/alpha25-evidence/underground-724-42-mine-create-required/create.json)
|
||||
et [rechargement](../build/alpha25-evidence/underground-724-42-mine-reload-required/reload.json).
|
||||
La cabane du mineur n'a toujours pas de témoin natif positif : son refus
|
||||
faute d'accès adéquat n'est pas présenté comme une génération réussie.
|
||||
|
||||
`./gradlew check build` passe avec **94 tâches et 24 GameTests obligatoires**
|
||||
([journal](../build/alpha25-check-build-final.log)). L'assemblage
|
||||
`./gradlew assemblePack -x :sanctuary:check` passe ensuite en une seconde,
|
||||
sans répéter les vérifications déjà réussies. Le
|
||||
[contrôle des artefacts](../build/alpha25-artifacts.json) et la
|
||||
[réception locale](../build/alpha25-delivery-local.json) vérifient le JAR,
|
||||
le MRpack **0.1.0-alpha.25** et le bootstrap Prism, conservant Minecraft
|
||||
**26.3-pre-2**. Le SHA-256 du MRpack est
|
||||
`7dcd58840714f19e3c0929c8618f7779b583ec1f76a1a8706f1183439d922754`.
|
||||
Ces reçus locaux ne déclarent ni publication ni modification de l'instance
|
||||
Prism ; ces deux opérations restent en cours.
|
||||
|
||||
Les limites ouvertes restent la cabane du mineur, l'admission dans du vide
|
||||
déjà exploré avec certificat, le contrôle final du Blocodex et l'essai visuel
|
||||
client. Les douze scénarios terrain et l'hydrologie réussis gardent leur portée
|
||||
décrite plus haut. Le benchmark demandé pour l'alpha.26 viendra après la
|
||||
livraison, sans retarder l'essai de cette version.
|
||||
|
||||
Les smokes déjà exécutés couvrent notamment géométrie, sols DRY, revêtement,
|
||||
appuis de hutte dans les quatre orientations, réservations d’eau et codec du
|
||||
monument. Ils restent distincts des preuves de terrain, habitants et fluides.
|
||||
Les essais historiques 23/création et 24/création-rechargement déjà exécutés
|
||||
sont archivés sous `historical*` ; ils ne prolongent pas les critères de réception.
|
||||
|
||||
## Parcours de visite facultatif
|
||||
|
||||
<details>
|
||||
<summary>Afficher des repères précis pour l'essai visuel — emplacements révélés</summary>
|
||||
|
||||
Créer des **nouveaux mondes Sanctuary**, avec le bouton natif **Personnaliser**,
|
||||
les tailles/graines du tableau et les deux options **Générer les structures**
|
||||
et **Structures expérimentales** activées. Activer les commandes et passer
|
||||
d'abord en spectateur :
|
||||
|
||||
```mcfunction
|
||||
/gamemode spectator
|
||||
```
|
||||
|
||||
Les points suivants servent à observer, pas à arriver en survie : ils peuvent
|
||||
être proches d'un plafond, dans l'eau ou au-dessus du vide. Attendre les chunks
|
||||
puis se déplacer pour examiner les raccords. Les preuves serveur n'équivalent
|
||||
pas à des captures visuelles et la distance de rendu conditionne la visibilité.
|
||||
|
||||
| Monde | Repère | Commande en spectateur |
|
||||
| --- | --- | --- |
|
||||
| Petit 512 / 0 | Berge inférieure vérifiée, eau Y160 | `/tp @s 104.5 162 -7.5` |
|
||||
| Petit 512 / 42 | Marais de surface, eau Y249 | `/tp @s -59.5 252 104.5` |
|
||||
| Petit 512 / 42 | Berge du lac inférieur Y189 | `/tp @s -103.5 190 4.5` |
|
||||
| Petit 512 / 42 | Hutte de surface habitée | `/tp @s -44 246 -125` |
|
||||
| Petit 512 / 18 | Entrée du village souterrain, FULL et rechargement vérifiés | `/tp @s -52 148 -5` |
|
||||
| Moyen 724 / 42 | Entrée de la mine native, FULL et rechargement vérifiés | `/tp @s 6 190 6` |
|
||||
| Moyen 724 / 0 | Montgolfière | `/tp @s -492 288 110` |
|
||||
| Moyen 724 / 0 | Navire de vivres | `/tp @s -35 186 -503` |
|
||||
| Moyen 724 / 0 | Navire de matériel | `/tp @s 463 186 -204` |
|
||||
| Moyen 724 / 0 | Îlot avec cascade | `/tp @s 399 237 333` |
|
||||
| Moyen 724 / 0 | Îlot sans cascade | `/tp @s -277 288 456` |
|
||||
| Moyen 724 / 0 | Approche du cadre minéral, air vérifié | `/tp @s 183.5 274 43.5` |
|
||||
|
||||
Dans les marais, regarder les berges, les petites plantes sous roche et la
|
||||
variation du biome F3 en montant ou en sortant de la poche. Pour les satellites,
|
||||
inspecter silhouettes, suspentes, ponts, filons et végétation. Le cadre minéral
|
||||
est inactif : aucune dimension ne s'ouvre. Les repères viennent des rapports
|
||||
de marais 512/0 et 512/42 cités plus haut et de
|
||||
[Moyen 724/0 en FULL](../build/alpha25-evidence/terrain-724-0-wetlands-final1/create.json).
|
||||
|
||||
Les commandes opérateur existantes `/sanctuary structures` et
|
||||
`/sanctuary census structures aerial` donnent les repères et la couverture du
|
||||
monde courant. Une structure recensée comme planifiée n'est pas nécessairement
|
||||
entièrement générée. Les repères du village et de la mine sont désormais
|
||||
confirmés par les cycles FULL cités plus haut. Le
|
||||
[guide local détaillé](../build/alpha25-player-witnesses.md), rédigé avant ces
|
||||
deux cycles, conserve les sources et points de rivage des autres lieux.
|
||||
|
||||
</details>
|
||||
|
||||
## Reproduire les scénarios existants
|
||||
|
||||
Exécuter les commandes **séquentiellement**. Sans `sanctuaryExpansionReload=true`,
|
||||
`runGameTest` recrée uniquement le monde de développement ignoré : ne jamais y
|
||||
placer une sauvegarde personnelle. Les mesures utilisent les schedulers natifs,
|
||||
sans sources reconstruites ni ticks forcés pour faire passer les assertions.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryWetlands25Tests=true -PsanctuaryWetlands25Required=true \
|
||||
-PsanctuaryWetlands25UndergroundRequired=true \
|
||||
-PsanctuaryTestDiameter=512 -PsanctuaryTestSeed=42
|
||||
# Pour chaque scénario OFF séparé : ajouter soit
|
||||
# -PsanctuaryGenerateStructures=false, soit -PsanctuaryExperimentalStructures=false.
|
||||
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryTerrain25Tests=true -PsanctuaryTestDiameter=512 \
|
||||
-PsanctuaryTestSeed=0 -PsanctuaryTerrain25Phase=create
|
||||
# Réouverture immédiate du même monde : remplacer create par reload1 (puis reload2)
|
||||
# et ajouter -PsanctuaryExpansionReload=true.
|
||||
# Un nouvel ordre se teste avec -PsanctuaryTerrain25ReverseChunks=true.
|
||||
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryHydrology25Tests=true -PsanctuaryTestDiameter=512 \
|
||||
-PsanctuaryTestSeed=0 -PsanctuaryHydrology25ExpectedFalls=2 \
|
||||
-PsanctuaryHydrology25ObservationTicks=1800
|
||||
# Témoins zéro/une sortie : 512/42 et 1024/2026 ; défaut 600 ticks, maximum 3600.
|
||||
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryMonument25Tests=true -PsanctuaryTestGeneration=25 \
|
||||
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryExperimentalStructures=false -PsanctuaryMonument25Diameter=512 \
|
||||
-PsanctuaryMonument25Climate=temperate -PsanctuaryMonument25Phase=create \
|
||||
-PsanctuaryMonument25AwaitReady=true
|
||||
# Réouverture immédiate : phase reload et -PsanctuaryExpansionReload=true.
|
||||
# Pour le scénario océan global OFF : GenerateStructures=false, AwaitReady=false.
|
||||
```
|
||||
|
||||
La préversion séparée `0.1.0-alpha.25-preview.1` (commit `817ab0b`) a passé
|
||||
`check build assemblePack`,24 GameTests, 95 chunks FULL et 1800 ticks d’eau.
|
||||
Son MRpack SHA-256 `a79e16d595906d636e0de16d4f6f1cc76df94145bbd4f5ed6747bc7de0a09429`
|
||||
est vérifié, sans auto-update ni déplacement du canal stable. Son codec
|
||||
`island_v25_preview_1` et ces preuves ne constituent pas la réception finale 25.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.24
|
||||
pack_version=0.1.0-alpha.24
|
||||
mod_version=0.1.0-alpha.25
|
||||
pack_version=0.1.0-alpha.25
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
+140
-2
@@ -40,7 +40,33 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : '24'))
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : ['sanctuaryAerial24Tests', 'sanctuaryAerial24FlowAdmission'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '24' : '25'))
|
||||
systemProperty('sanctuary.test.terrain25', providers.gradleProperty('sanctuaryTerrain25Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.hydrology25', providers.gradleProperty('sanctuaryHydrology25Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.hydrology25.expectedFalls', providers.gradleProperty('sanctuaryHydrology25ExpectedFalls').getOrElse('-1'))
|
||||
systemProperty('sanctuary.test.hydrology25.observationTicks', providers.gradleProperty('sanctuaryHydrology25ObservationTicks').getOrElse('600'))
|
||||
systemProperty('sanctuary.test.wetlands25.required', providers.gradleProperty('sanctuaryWetlands25Required').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.wetlands25.undergroundRequired', providers.gradleProperty('sanctuaryWetlands25UndergroundRequired').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.benchmark.fixedCoordinates', providers.gradleProperty('sanctuaryBenchmarkFixedCoordinates').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.terrain25.phase', providers.gradleProperty('sanctuaryTerrain25Phase').getOrElse('create'))
|
||||
systemProperty('sanctuary.test.terrain25.reverseChunks', providers.gradleProperty('sanctuaryTerrain25ReverseChunks').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.terrain25.requireMooshrooms', providers.gradleProperty('sanctuaryTerrain25RequireMooshrooms').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.terrain25.requireWaterLake', providers.gradleProperty('sanctuaryTerrain25RequireWaterLake').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.terrain25.requireLavaLake', providers.gradleProperty('sanctuaryTerrain25RequireLavaLake').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.underground25', providers.gradleProperty('sanctuaryUnderground25Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.monument25', providers.gradleProperty('sanctuaryMonument25Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.monument25.diameter', providers.gradleProperty('sanctuaryMonument25Diameter').getOrElse('512'))
|
||||
systemProperty('sanctuary.test.monument25.climate', providers.gradleProperty('sanctuaryMonument25Climate').getOrElse('temperate'))
|
||||
systemProperty('sanctuary.test.monument25.phase', providers.gradleProperty('sanctuaryMonument25Phase').getOrElse('create'))
|
||||
systemProperty('sanctuary.test.monument25.awaitReady', providers.gradleProperty('sanctuaryMonument25AwaitReady').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.underground25.planOnly', providers.gradleProperty('sanctuaryUnderground25PlanOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.underground25.required', providers.gradleProperty('sanctuaryUnderground25Required').getOrElse(''))
|
||||
systemProperty('sanctuary.test.underground25.phase', providers.gradleProperty('sanctuaryUnderground25Phase').getOrElse('create'))
|
||||
systemProperty('sanctuary.test.underground25.search.seeds', providers.gradleProperty('sanctuaryUnderground25SearchSeeds').getOrElse('1,2,3,4,5,6'))
|
||||
systemProperty('sanctuary.test.underground25.search.diameter', providers.gradleProperty('sanctuaryUnderground25SearchDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.underground25.search.required', providers.gradleProperty('sanctuaryUnderground25SearchRequired').getOrElse(''))
|
||||
systemProperty('sanctuary.test.underground25.search.dryOnly', providers.gradleProperty('sanctuaryUnderground25SearchDryOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.underground25.search.stopAfterFirstDry', providers.gradleProperty('sanctuaryUnderground25SearchStopAfterFirstDry').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24.flowAdmission', providers.gradleProperty('sanctuaryAerial24FlowAdmission').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24', providers.gradleProperty('sanctuaryAerial24Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24.reverseChunks', providers.gradleProperty('sanctuaryAerial24ReverseChunks').getOrElse('false'))
|
||||
@@ -56,6 +82,7 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.generateStructures', providers.gradleProperty('sanctuaryGenerateStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
|
||||
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
|
||||
@@ -98,6 +125,20 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('heritage21', heritage21)
|
||||
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity21', lostCity21)
|
||||
def terrain25 = providers.gradleProperty('sanctuaryTerrain25Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('terrain25', terrain25)
|
||||
def hydrology25 = providers.gradleProperty('sanctuaryHydrology25Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('hydrology25', hydrology25)
|
||||
def wetlands25 = providers.gradleProperty('sanctuaryWetlands25Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('wetlands25', wetlands25)
|
||||
def underground25Search = providers.gradleProperty('sanctuaryUnderground25Search').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('underground25Search', underground25Search)
|
||||
def terrain25Diagnostics = providers.gradleProperty('sanctuaryTerrain25Diagnostics').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('terrain25Diagnostics', terrain25Diagnostics)
|
||||
def monument25 = providers.gradleProperty('sanctuaryMonument25Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('monument25', monument25)
|
||||
def underground25 = providers.gradleProperty('sanctuaryUnderground25Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('underground25', underground25)
|
||||
def aerial24Flow = providers.gradleProperty('sanctuaryAerial24FlowAdmission').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('aerial24Flow', aerial24Flow)
|
||||
def aerial24 = providers.gradleProperty('sanctuaryAerial24Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -119,7 +160,7 @@ tasks.named('processGametestResources') {
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
expand(gametest_entrypoint: underground25Search ? 'fr.koka.sanctuary.gametest.Underground25SeedSearch' : wetlands25 ? 'fr.koka.sanctuary.gametest.Wetlands25WorldGameTests' : hydrology25 ? 'fr.koka.sanctuary.gametest.Hydrology25WorldGameTests' : terrain25 ? 'fr.koka.sanctuary.gametest.Terrain25WorldGameTests' : terrain25Diagnostics ? 'fr.koka.sanctuary.gametest.Terrain25Diagnostics' : monument25 ? 'fr.koka.sanctuary.gametest.Expansion25MonumentGameTests' : underground25 ? 'fr.koka.sanctuary.gametest.Underground25WorldGameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
@@ -613,3 +654,100 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
|
||||
|
||||
// Generation25 keeps its geometry tests separate from the historical witnesses.
|
||||
tasks.register('mineral25Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.mineral.Mineral25Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('mineral25Smoke') }
|
||||
|
||||
tasks.register('terrain25ShapeSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25ShapeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25ShapeSmoke') }
|
||||
|
||||
tasks.register('expansionMonument25Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionMonument25Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionMonument25Smoke') }
|
||||
|
||||
tasks.register('initialFlows25Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionInitialFlows25Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('initialFlows25Smoke') }
|
||||
|
||||
tasks.register('terrain25CoatingSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25CoatingSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25CoatingSmoke') }
|
||||
|
||||
tasks.register('underground25Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.underground25.Underground25Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('underground25Smoke') }
|
||||
|
||||
tasks.register('terrain25ProtectionSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25ProtectionSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25ProtectionSmoke') }
|
||||
|
||||
tasks.register('terrain25ColumnBoundsSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25ColumnBoundsSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25ColumnBoundsSmoke') }
|
||||
|
||||
tasks.register('terrain25DryFloorSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25DryFloorSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25DryFloorSmoke') }
|
||||
|
||||
tasks.register('terrain25WetlandsSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25WetlandsSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25WetlandsSmoke') }
|
||||
|
||||
tasks.register('terrain25WetlandSoilSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25WetlandSoilSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25WetlandSoilSmoke') }
|
||||
|
||||
tasks.register('terrain25WetlandPlantLightSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terrain25WetlandPlantLightSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terrain25WetlandPlantLightSmoke') }
|
||||
|
||||
+1
@@ -155,6 +155,7 @@ public final class Aerial24WorldGameTests {
|
||||
report.put("expansion_journal_unchanged", true);
|
||||
StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
|
||||
report.put("fluid_scheduler", FluidTiming25.snapshot());
|
||||
report.put("passed", true);
|
||||
write(world.resolve("aerial24-" + phase + ".json"), report);
|
||||
write(Path.of("diagnostics/aerial24/" + phase + ".json"), report);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
/** Native loading and simulation tickets, including dimension activity, without persisted force-load state. */
|
||||
final class ChunkTickets25 {
|
||||
private ChunkTickets25() {}
|
||||
static void add(ServerLevel level, ChunkPos pos) {
|
||||
level.getChunkSource().addTicketWithRadius(TicketType.PLAYER_LOADING, pos, 2);
|
||||
level.getChunkSource().addTicketWithRadius(TicketType.PLAYER_SIMULATION, pos, 2);
|
||||
}
|
||||
static void remove(ServerLevel level, ChunkPos pos) {
|
||||
level.getChunkSource().removeTicketWithRadius(TicketType.PLAYER_LOADING, pos, 2);
|
||||
level.getChunkSource().removeTicketWithRadius(TicketType.PLAYER_SIMULATION, pos, 2);
|
||||
}
|
||||
}
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.expansion.*;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.tags.StructureTags;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.entity.SpawnPlacements;
|
||||
import net.minecraft.world.entity.monster.Guardian;
|
||||
import net.minecraft.world.level.NaturalSpawner;
|
||||
import net.minecraft.world.entity.monster.ElderGuardian;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real admission plus a bounded FULL monument footprint; the whole-island preparation queue is tested separately. */
|
||||
public final class Expansion25MonumentGameTests {
|
||||
@GameTest(maxTicks = 60000)
|
||||
public void reservedMonumentAndEldersSurviveReload(GameTestHelper helper) throws Exception {
|
||||
new Scenario(helper).start();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final net.minecraft.server.level.ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionRuntime.Session session;
|
||||
final boolean reload = !System.getProperty("sanctuary.test.monument25.phase", "create").equals("create");
|
||||
final int diameter = Integer.getInteger("sanctuary.test.monument25.diameter", 512);
|
||||
final String climate = System.getProperty("sanctuary.test.monument25.climate", "temperate");
|
||||
final boolean awaitReady = Boolean.getBoolean("sanctuary.test.monument25.awaitReady");
|
||||
final Path world, directory, journal, proof;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
ExpansionIsland island;
|
||||
ExpansionStructurePlan25 plan;
|
||||
JsonObject previous;
|
||||
int index, wait;
|
||||
long entityWaitStartedNanos, entityReadyNanos;
|
||||
final long scenarioStartedNanos = System.nanoTime();
|
||||
long readyWaitStartedNanos, readyLastAdvanceNanos, readyLastReportNanos;
|
||||
int readyLastIndex = -1, readyTotalChunks = -1;
|
||||
boolean tickets;
|
||||
Scenario(GameTestHelper helper) {
|
||||
this.helper = helper; level = helper.getLevel(); generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
session = ExpansionRuntime.session(level);
|
||||
world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
directory = world.resolve("data").resolve(generator.journalDirectory()); journal = directory.resolve(ExpansionJournal.FILE_NAME);
|
||||
proof = world.resolve("monument25-created.json");
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("diameter", diameter); report.put("climate", climate);
|
||||
report.put("phase", reload ? "reload" : "create"); report.put("world_seed", level.getSeed());
|
||||
report.put("await_ready", awaitReady);
|
||||
}
|
||||
void start() throws Exception {
|
||||
helper.assertTrue(Boolean.getBoolean("sanctuary.test.monument25") && generator.rootGeneration() == 25,
|
||||
"Dedicated disposable generation25 world required");
|
||||
try {
|
||||
if (!level.structureManager().shouldGenerateStructures()) { verifyStructuresOff(); return; }
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof)).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload uses a fresh JVM");
|
||||
var saved = ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, previous.get("island")).getOrThrow();
|
||||
helper.assertTrue(saved.diameter() == diameter && saved.climate().equals(climate), "Reload arguments match the saved monument scenario");
|
||||
island = session.island(saved.id());
|
||||
helper.assertTrue(island.ready().equals(saved.ready()), "Startup restores the exact admitted geometry and plan");
|
||||
report.put("create_called", false);
|
||||
} else {
|
||||
helper.assertTrue(session.islands().size() == 1 && !Files.exists(proof), "Fresh world, no preview before admission");
|
||||
byte[] pristine = Files.readAllBytes(journal);
|
||||
var attempts = new ArrayList<Map<String, Object>>(); report.put("attempts", attempts);
|
||||
for (int draw = 0; draw < 3 && island == null; draw++) {
|
||||
long start = System.nanoTime();
|
||||
try {
|
||||
island = session.create("monument25_" + draw, "sanctuary", "east", climate, diameter, "ocean", 4096);
|
||||
long elapsed = (System.nanoTime() - start) / 1_000_000;
|
||||
attempts.add(Map.of("draw", draw, "milliseconds", elapsed, "accepted", true));
|
||||
helper.assertTrue(elapsed < 31_000, "Real admission uses its thirty-second deadline");
|
||||
} catch (IllegalArgumentException refused) {
|
||||
helper.assertTrue(Arrays.equals(pristine, Files.readAllBytes(journal)) && session.islands().size() == 1,
|
||||
"A refused ocean leaves no reservation");
|
||||
attempts.add(Map.of("draw", draw, "milliseconds", (System.nanoTime() - start) / 1_000_000, "reason", String.valueOf(refused.getMessage())));
|
||||
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(island != null, "A suitable monument must be found within three bounded requests");
|
||||
var bytes = Files.readAllBytes(journal);
|
||||
helper.assertTrue(session.create(island.id(), "sanctuary", "east", climate, diameter, "ocean", 4096).equals(island)
|
||||
&& Arrays.equals(bytes, Files.readAllBytes(journal)), "Repeated admission cannot redraw the selected monument");
|
||||
}
|
||||
plan = island.structurePlan25().orElseThrow();
|
||||
helper.assertTrue(new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 25).islands().contains(island), "Durable journal contains the exact plan");
|
||||
helper.assertTrue(plan.structure().equals("minecraft:monument"), "Ocean guarantee cannot be satisfied by a ruin or shipwreck");
|
||||
if (!island.reuseToken().isEmpty()) {
|
||||
String certificate = Files.readString(directory.resolve("void-reuse").resolve(island.reuseToken() + ".json"));
|
||||
helper.assertTrue(ExpansionVoidCertificate.digest(certificate).equals(island.reuseToken()), "Certificate digest");
|
||||
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(certificate).getAsJsonObject().get("island")).getOrThrow()
|
||||
.equals(island.withReuseToken("").ready()), "Certificate includes native pieces and algorithm");
|
||||
}
|
||||
chunks.addAll(plan.protection().intersectingChunks().toList()); chunks.add(new ChunkPos(0, 0));
|
||||
helper.runAfterDelay(1, this::step);
|
||||
} catch (Exception error) { fail(error); }
|
||||
}
|
||||
/** Real admission with the global Minecraft switch OFF; no native preflight, fake plan or fabricated READY. */
|
||||
void verifyStructuresOff() throws Exception {
|
||||
helper.assertTrue(!reload && !awaitReady && session.islands().size() == 1,
|
||||
"Structures-OFF admission uses a fresh bounded scenario without a whole-island READY claim");
|
||||
long admissionStarted = System.nanoTime();
|
||||
island = session.create("monument25_off", "sanctuary", "east", climate, diameter, "ocean", 4096);
|
||||
report.put("admission_milliseconds", (System.nanoTime() - admissionStarted) / 1_000_000);
|
||||
helper.assertTrue(island.structurePlan25().isEmpty(), "Real structures-OFF admission captures no monument");
|
||||
var encoded = ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, island).getOrThrow();
|
||||
helper.assertTrue(!encoded.getAsJsonObject().has("structure_plan25"), "The codec invents no monument payload with structures OFF");
|
||||
var savedJournal = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 25);
|
||||
helper.assertTrue(savedJournal.islands().contains(island) && savedJournal.islands().stream().allMatch(i -> i.structurePlan25().isEmpty()),
|
||||
"The durable OFF journal has no captured monument plan");
|
||||
helper.assertTrue(!Files.readString(journal).contains("structure_plan25"), "The manual journal writer omits absent structure plans");
|
||||
if (!island.reuseToken().isEmpty()) {
|
||||
String certificate = Files.readString(directory.resolve("void-reuse").resolve(island.reuseToken() + ".json"));
|
||||
helper.assertTrue(ExpansionVoidCertificate.digest(certificate).equals(island.reuseToken()), "OFF certificate digest");
|
||||
var certified = JsonParser.parseString(certificate).getAsJsonObject().getAsJsonObject("island");
|
||||
helper.assertTrue(!certified.has("structure_plan25") && ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, certified).getOrThrow()
|
||||
.equals(island.withReuseToken("").ready()), "Any OFF void certificate preserves the exact geometry without native pieces");
|
||||
}
|
||||
report.put("certificate_present", !island.reuseToken().isEmpty());
|
||||
var starts = ExpansionStructures23.candidates(island);
|
||||
helper.assertTrue(!starts.isEmpty() && starts.size() <= 128, "Finite native start candidate set for this OFF ocean");
|
||||
for (var candidate : starts) {
|
||||
var at = candidate.chunk();
|
||||
var chunk = level.getChunk(at.x(), at.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
helper.assertTrue(chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid),
|
||||
"GlobalOFF emits no native start at a candidate chunk: " + at);
|
||||
}
|
||||
var full = new java.util.LinkedHashSet<ChunkPos>();
|
||||
full.add(new ChunkPos(island.centerX() >> 4, island.centerZ() >> 4));
|
||||
full.add(starts.getFirst().chunk()); full.add(starts.get(starts.size() / 2).chunk());
|
||||
long actualWater = 0;
|
||||
for (var at : full) {
|
||||
var chunk = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid), "GlobalOFF keeps FULL chunks structure-free");
|
||||
for (var pos : BlockPos.betweenClosed(at.getMinBlockX(), 0, at.getMinBlockZ(), at.getMaxBlockX(), 383, at.getMaxBlockZ())) {
|
||||
var state = chunk.getBlockState(pos);
|
||||
if (state.is(Blocks.WATER)) actualWater++;
|
||||
helper.assertTrue(!state.is(Blocks.PRISMARINE) && !state.is(Blocks.PRISMARINE_BRICKS)
|
||||
&& !state.is(Blocks.DARK_PRISMARINE) && !state.is(Blocks.SEA_LANTERN), "GlobalOFF does not place monument blocks");
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualWater > 0, "GlobalOFF retains a real floating ocean");
|
||||
byte[] before = Files.readAllBytes(journal);
|
||||
var beforeSession = List.copyOf(session.islands()); var beforePublished = List.copyOf(generator.source().islands());
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var wanted = registry.getOrThrow(StructureTags.ON_OCEAN_MONUMENT_MAPS);
|
||||
var origin = new BlockPos(island.centerX(), ExpansionOcean23.seaLevel(island), island.centerZ());
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, wanted, origin, 8, false) == null
|
||||
&& generator.findNearestMapStructure(level, wanted, origin, 8, true) == null, "Both normal and explorer-map tag searches are empty with structures OFF");
|
||||
helper.assertTrue(session.create(island.id(), "sanctuary", "east", climate, diameter, "ocean", 4096).equals(session.island(island.id())),
|
||||
"Repeated OFF admission reuses its saved reservation");
|
||||
helper.assertTrue(Arrays.equals(before, Files.readAllBytes(journal)) && beforeSession.equals(session.islands())
|
||||
&& beforePublished.equals(generator.source().islands()), "OFF searches and duplicate request never activate or redraw an expansion");
|
||||
report.put("generate_structures", false); report.put("native_candidates_checked", starts.size()); report.put("full_chunks_checked", full.size());
|
||||
report.put("actual_water_blocks", actualWater); report.put("monument_plan_captured", false);
|
||||
report.put("locator_empty", true); report.put("search_journal_unchanged", true); report.put("whole_island_ready_checked", false);
|
||||
report.put("island", ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, session.island(island.id())).getOrThrow());
|
||||
report.put("progress", session.progress()); report.put("scenario_milliseconds", (System.nanoTime() - scenarioStartedNanos) / 1_000_000);
|
||||
report.put("passed", true); write("global-off", new GsonBuilder().setPrettyPrinting().create().toJson(report)); helper.succeed();
|
||||
}
|
||||
void step() {
|
||||
try {
|
||||
if (index < chunks.size()) {
|
||||
var pos = chunks.get(index++); report.put("stage", "FULL " + pos);
|
||||
level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true); helper.runAfterDelay(1, this::step); return;
|
||||
}
|
||||
if (!tickets) {
|
||||
chunks.forEach(pos -> ChunkTickets25.add(level, pos));
|
||||
tickets = true; entityWaitStartedNanos = System.nanoTime();
|
||||
}
|
||||
boolean ready = recordReadiness();
|
||||
var elders = level.getEntitiesOfClass(ElderGuardian.class, AABB.of(plan.protection()).inflate(12));
|
||||
int expected = reload ? 1 : 3;
|
||||
report.put("elders_observed", elders.stream().map(e -> Map.of("uuid", e.getUUID().toString(), "position", List.of(e.getX(), e.getY(), e.getZ()))).toList());
|
||||
report.put("entity_wait_ticks", wait);
|
||||
if ((!ready || elders.size() != expected) && wait++ < 6000) { report.put("stage", "entity readiness"); helper.runAfterDelay(1, this::step); return; }
|
||||
helper.assertTrue(ready, "All monument entity chunks and the extraction chunk are loaded and ticking: " + report.get("pending_entity_chunks"));
|
||||
helper.assertTrue(elders.size() == expected, "Three initial elders; no replacements after one death and one extraction");
|
||||
if (entityReadyNanos == 0) entityReadyNanos = System.nanoTime();
|
||||
if (awaitReady && !recordWholeIslandReadiness()) {
|
||||
helper.runAfterDelay(5, this::step); return;
|
||||
}
|
||||
inspect(elders);
|
||||
fr.koka.sanctuary.knowledge.structures.StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist native starts, mutations and entities");
|
||||
report.put("island", ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, session.island(island.id())).getOrThrow());
|
||||
report.put("full_monument_chunks", chunks.size()); report.put("whole_island_ready_checked", awaitReady);
|
||||
report.put("scenario_milliseconds", (System.nanoTime() - scenarioStartedNanos) / 1_000_000); report.put("passed", true);
|
||||
String json = new GsonBuilder().setPrettyPrinting().create().toJson(report);
|
||||
write(reload ? "reload" : "create", json);
|
||||
if (!reload) Files.writeString(proof, json);
|
||||
release(); helper.succeed();
|
||||
} catch (Exception error) { fail(error); }
|
||||
}
|
||||
/** Observe the production queue; do not publish an artificial READY snapshot or load its remainder in this fixture. */
|
||||
boolean recordWholeIslandReadiness() throws java.io.IOException {
|
||||
long now = System.nanoTime();
|
||||
if (readyWaitStartedNanos == 0) { readyWaitStartedNanos = now; readyLastAdvanceNanos = now; }
|
||||
var current = session.island(island.id());
|
||||
report.put("preparation_progress", session.progress());
|
||||
report.put("ready_wait_milliseconds", (now - readyWaitStartedNanos) / 1_000_000);
|
||||
report.put("ready_wait_budget_milliseconds", 900_000);
|
||||
report.put("ready_stall_budget_milliseconds", 300_000);
|
||||
if (current.status().equals("ready")) {
|
||||
helper.assertTrue(current.equals(island.ready()), "Production READY preserves the admitted monument, priorities and complete island descriptor");
|
||||
helper.assertTrue(new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 25).islands().contains(current),
|
||||
"The production queue commits READY durably before the locator can advertise it");
|
||||
helper.assertTrue(generator.source().islands().contains(current), "The generator publishes the same durably READY island");
|
||||
if (readyTotalChunks >= 0) report.put("ready_full_chunks", readyTotalChunks);
|
||||
report.put("already_ready_at_first_observation", readyTotalChunks < 0);
|
||||
report.put("ready_observed", true);
|
||||
return true;
|
||||
}
|
||||
report.put("stage", "wait for real whole-island READY");
|
||||
var progress = java.util.regex.Pattern.compile(java.util.regex.Pattern.quote(island.id()) + ": (\\d+)/(\\d+)").matcher(session.progress());
|
||||
helper.assertTrue(progress.matches(), "Whole-island preparation must continue: " + session.progress());
|
||||
int completed = Integer.parseInt(progress.group(1)), total = Integer.parseInt(progress.group(2));
|
||||
helper.assertTrue(completed >= readyLastIndex && completed < total && total > 0
|
||||
&& (readyTotalChunks < 0 || readyTotalChunks == total), "Production preparation is monotonic over a stable finite chunk list");
|
||||
if (completed > readyLastIndex) { readyLastAdvanceNanos = now; readyLastIndex = completed; }
|
||||
readyTotalChunks = total;
|
||||
report.put("ready_completed_chunks", completed); report.put("ready_total_chunks", total);
|
||||
report.put("ready_stalled_milliseconds", (now - readyLastAdvanceNanos) / 1_000_000);
|
||||
helper.assertTrue(now - readyWaitStartedNanos < 900_000_000_000L, "Real whole-island preparation exceeded its fifteen-minute test budget");
|
||||
helper.assertTrue(now - readyLastAdvanceNanos < 300_000_000_000L, "The production queue made no FULL-chunk progress for five minutes");
|
||||
if (readyLastReportNanos == 0 || now - readyLastReportNanos >= 15_000_000_000L) {
|
||||
write("preparation", new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
readyLastReportNanos = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
boolean recordReadiness() {
|
||||
var statuses = new ArrayList<Map<String, Object>>();
|
||||
var pending = new ArrayList<String>();
|
||||
for (var pos : chunks) {
|
||||
boolean loaded = level.areEntitiesLoaded(pos.pack());
|
||||
boolean ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
|
||||
statuses.add(Map.of("x", pos.x(), "z", pos.z(), "entities_loaded", loaded,
|
||||
"entities_ticking", ticking, "chunk_debug", level.getChunkSource().getChunkDebugData(pos),
|
||||
"extraction_chunk", pos.x() == 0 && pos.z() == 0));
|
||||
if (!loaded || !ticking) pending.add(pos.toString());
|
||||
}
|
||||
report.put("entity_chunks", statuses); report.put("pending_entity_chunks", pending);
|
||||
report.put("entity_wait_milliseconds", entityWaitStartedNanos == 0 ? 0
|
||||
: ((entityReadyNanos == 0 ? System.nanoTime() : entityReadyNanos) - entityWaitStartedNanos) / 1_000_000);
|
||||
return pending.isEmpty();
|
||||
}
|
||||
void inspect(List<ElderGuardian> elders) throws java.io.IOException {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var holder = registry.get(Identifier.parse("minecraft:monument")).orElseThrow();
|
||||
var chunk = level.getChunk(plan.chunkX(), plan.chunkZ());
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
helper.assertTrue(start != null && start.isValid(), "Reserved start is really created at STRUCTURE_STARTS");
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var tag = start.createTag(context, start.getChunkPos());
|
||||
var withoutReferences = tag.copy(); withoutReferences.putInt("references", 0);
|
||||
helper.assertTrue(withoutReferences.equals(plan.tag()), "Native generated pieces equal the pre-admission payload");
|
||||
long prismarine = 0, lanterns = 0, gold = 0; BlockPos removable = null;
|
||||
for (var pos : BlockPos.betweenClosed(plan.minX(), plan.minY(), plan.minZ(), plan.maxX(), plan.maxY(), plan.maxZ())) {
|
||||
var block = level.getBlockState(pos);
|
||||
if (block.is(Blocks.PRISMARINE) || block.is(Blocks.PRISMARINE_BRICKS) || block.is(Blocks.DARK_PRISMARINE)) { prismarine++; if (removable == null) removable = pos.immutable(); }
|
||||
if (block.is(Blocks.SEA_LANTERN)) lanterns++;
|
||||
if (block.is(Blocks.GOLD_BLOCK)) gold++;
|
||||
}
|
||||
helper.assertTrue(prismarine > 100 && lanterns > 0 && gold >= 8, "Actual FULL contains a detailed vanilla monument and its eight gold blocks");
|
||||
report.put("blocks", Map.of("prismarine", prismarine, "sea_lantern", lanterns, "gold_block", gold));
|
||||
verifyImmersion();
|
||||
verifyOrdinaryGuardians(start);
|
||||
verifyLocator(start);
|
||||
if (!reload) {
|
||||
elders = elders.stream().sorted(Comparator.comparing(e -> e.getUUID().toString())).toList();
|
||||
String killed = elders.get(0).getUUID().toString(); elders.get(0).kill(level);
|
||||
var extracted = elders.get(1); extracted.setNoAi(true); extracted.setNoGravity(true); extracted.setPermanentlyInvulnerable(true);
|
||||
extracted.setDeltaMovement(Vec3.ZERO); extracted.snapTo(4.5, 330, 4.5); extracted.setPersistenceRequired();
|
||||
report.put("killed_uuid", killed); report.put("extracted_uuid", extracted.getUUID().toString());
|
||||
report.put("remaining_uuid", elders.get(2).getUUID().toString());
|
||||
helper.assertTrue(removable != null, "A real monument block is available for destructive persistence testing");
|
||||
level.setBlock(removable, Blocks.AIR.defaultBlockState(), 2);
|
||||
report.put("removed_block", List.of(removable.getX(), removable.getY(), removable.getZ()));
|
||||
} else {
|
||||
helper.assertTrue(level.getEntity(UUID.fromString(previous.get("killed_uuid").getAsString())) == null, "Killed elder stays absent");
|
||||
var extracted = level.getEntity(UUID.fromString(previous.get("extracted_uuid").getAsString()));
|
||||
helper.assertTrue(extracted instanceof ElderGuardian && extracted.getX() < 16 && extracted.getZ() < 16, "The extracted elder survives outside the monument");
|
||||
helper.assertTrue(elders.getFirst().getUUID().toString().equals(previous.get("remaining_uuid").getAsString()), "Only the untouched original elder remains");
|
||||
var at = previous.getAsJsonArray("removed_block"); var state = level.getBlockState(new BlockPos(at.get(0).getAsInt(), at.get(1).getAsInt(), at.get(2).getAsInt()));
|
||||
helper.assertTrue(!state.is(Blocks.PRISMARINE) && !state.is(Blocks.PRISMARINE_BRICKS) && !state.is(Blocks.DARK_PRISMARINE), "Demolished monument block is not restored");
|
||||
helper.assertTrue(start.getReferences() == previous.get("references").getAsInt(), "Reload preserves map reference counter");
|
||||
}
|
||||
report.put("native_tag", start.createTag(context, start.getChunkPos()).toString());
|
||||
var census = fr.koka.sanctuary.knowledge.structures.StructureCensusService.snapshot(level.getServer());
|
||||
var key = new fr.koka.sanctuary.knowledge.structures.StructureCensusMemory.SiteKey(level.dimension().identifier().toString(), "minecraft:monument", plan.chunkX(), 0, plan.chunkZ(), false);
|
||||
var evidence = census.evidence().sites().get(key);
|
||||
helper.assertTrue(evidence != null && census.evidence().generated(evidence), "Census records the generated monument");
|
||||
}
|
||||
/** Inspect water outside the native body and above its highest possible room; doorway air is not an exterior witness. */
|
||||
void verifyImmersion() {
|
||||
int seaLevel = ExpansionOcean23.seaLevel(island);
|
||||
helper.assertTrue(plan.maxY() + 1 < seaLevel, "The entire native monument fits below its ocean waterline");
|
||||
var immersion = new LinkedHashMap<String, Object>(); report.put("immersion", immersion);
|
||||
immersion.put("waterline", seaLevel); immersion.put("roof_y", plan.maxY());
|
||||
immersion.put("scope", "Actual FULL blocks on all four sides, three blocks outside the body, and across the water above the roof. Interior rooms and doorway openings are excluded. Native ice counts as frozen water.");
|
||||
var samples = new ArrayList<Map<String, Object>>(); immersion.put("samples", samples);
|
||||
var counts = new LinkedHashMap<String, Integer>();
|
||||
// The first level is directly above the native bounding box; subsequent levels
|
||||
// prove a water column up towards the surface, including frozen ocean material.
|
||||
var roofLevels = new java.util.TreeSet<Integer>();
|
||||
roofLevels.add(plan.maxY() + 1); roofLevels.add((plan.maxY() + 1 + seaLevel - 1) / 2); roofLevels.add(seaLevel - 1);
|
||||
for (int z = plan.minZ() + 4; z <= plan.maxZ() - 4; z += 8)
|
||||
for (int x = plan.minX() + 4; x <= plan.maxX() - 4; x += 8)
|
||||
for (int y : roofLevels) immersionSample(new BlockPos(x, y, z), "above_roof", samples, counts);
|
||||
int[] levels = {plan.minY() + 6, plan.minY() + 14, plan.maxY() + 1};
|
||||
for (int offset = 8; offset <= 48; offset += 10) for (int y : levels) {
|
||||
immersionSample(new BlockPos(plan.minX() - 3, y, plan.minZ() + offset), "west", samples, counts);
|
||||
immersionSample(new BlockPos(plan.maxX() + 3, y, plan.minZ() + offset), "east", samples, counts);
|
||||
immersionSample(new BlockPos(plan.minX() + offset, y, plan.minZ() - 3), "north", samples, counts);
|
||||
immersionSample(new BlockPos(plan.minX() + offset, y, plan.maxZ() + 3), "south", samples, counts);
|
||||
}
|
||||
immersion.put("counts", counts);
|
||||
helper.assertTrue(counts.getOrDefault("water", 0) > 0, "Actual water, not only a planned waterline, surrounds the monument");
|
||||
immersion.put("passed", true);
|
||||
}
|
||||
void immersionSample(BlockPos pos, String zone, List<Map<String, Object>> samples, Map<String, Integer> counts) {
|
||||
var state = level.getBlockState(pos);
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER);
|
||||
boolean frozen = state.is(Blocks.ICE) || state.is(Blocks.PACKED_ICE) || state.is(Blocks.BLUE_ICE) || state.is(Blocks.FROSTED_ICE);
|
||||
samples.add(Map.of("zone", zone, "position", List.of(pos.getX(), pos.getY(), pos.getZ()),
|
||||
"block", net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(),
|
||||
"water", water, "frozen", frozen));
|
||||
helper.assertTrue(water || frozen, "Monument immersion requires real water or native ice at " + zone + " " + pos + ": " + state);
|
||||
counts.merge(zone, 1, Integer::sum); counts.merge(water ? "water" : "frozen", 1, Integer::sum);
|
||||
}
|
||||
/** Exercise the real structure override and NaturalSpawner, without inventing a guardian quota or bypassing water rules. */
|
||||
@SuppressWarnings("removal")
|
||||
void verifyOrdinaryGuardians(StructureStart start) {
|
||||
var override = start.getStructure().spawnOverrides().get(MobCategory.MONSTER);
|
||||
helper.assertTrue(override != null && override.spawns().unwrap().stream()
|
||||
.anyMatch(entry -> entry.value().type() == EntityTypes.GUARDIAN), "The native monument retains its ordinary guardian spawn override");
|
||||
var witnesses = new ArrayList<BlockPos>();
|
||||
for (var pos : BlockPos.betweenClosed(plan.minX() + 2, plan.minY() + 1, plan.minZ() + 2,
|
||||
plan.maxX() - 2, plan.maxY() - 1, plan.maxZ() - 2)) {
|
||||
if (!level.getFluidState(pos).is(FluidTags.WATER) || !level.getFluidState(pos.below()).is(FluidTags.WATER)
|
||||
|| !SpawnPlacements.isSpawnPositionOk(EntityTypes.GUARDIAN, level, pos)
|
||||
|| !level.noCollision(EntityTypes.GUARDIAN.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5))) continue;
|
||||
var actual = generator.getMobsAt(level, level.structureManager(), MobCategory.MONSTER, pos);
|
||||
helper.assertTrue(actual.equals(override.spawns()), "Real monument references select the exact vanilla monster spawn table");
|
||||
witnesses.add(pos.immutable());
|
||||
if (witnesses.size() == 32) break;
|
||||
}
|
||||
helper.assertTrue(!witnesses.isEmpty(), "Generated monument contains water positions that support ordinary guardian spawning");
|
||||
var ordinary = new LinkedHashMap<String, Object>(); report.put("ordinary_guardians", ordinary);
|
||||
ordinary.put("water_witnesses", witnesses.stream().map(p -> List.of(p.getX(), p.getY(), p.getZ())).toList());
|
||||
ordinary.put("spawn_table", override.spawns().unwrap().stream().map(e -> Map.of(
|
||||
"type", e.value().type().toShortString(), "weight", e.weight(), "count", e.value().count().toString())).toList());
|
||||
ordinary.put("scope", "Bounded NaturalSpawner batches at real water positions; native table, spawn rules, collision and player distance. This does not measure the global mob cap or farm throughput.");
|
||||
var spawned = new ArrayList<Guardian>();
|
||||
var player = helper.makeMockServerPlayerInLevel();
|
||||
try {
|
||||
player.snapTo((plan.minX() + plan.maxX()) / 2.0 + 0.5, plan.maxY() + 64,
|
||||
(plan.minZ() + plan.maxZ()) / 2.0 + 0.5);
|
||||
helper.assertTrue(level.getNearestPlayer(player.getX(), player.getY(), player.getZ(), 1, false) == player,
|
||||
"The native spawner sees its nearby nonspectator test player");
|
||||
int batches = 0;
|
||||
while (spawned.isEmpty() && batches < 128) {
|
||||
var pos = witnesses.get(batches++ % witnesses.size());
|
||||
NaturalSpawner.spawnCategoryForPosition(MobCategory.MONSTER, level, level.getChunk(pos), pos,
|
||||
(type, world, candidate, sourceChunk) -> type == EntityTypes.GUARDIAN && start.getBoundingBox().isInside(candidate),
|
||||
(mob, sourceChunk) -> { if (mob instanceof Guardian guardian && !(mob instanceof ElderGuardian)) spawned.add(guardian); });
|
||||
}
|
||||
ordinary.put("batches", batches);
|
||||
ordinary.put("spawned", spawned.stream().map(g -> Map.of("uuid", g.getUUID().toString(),
|
||||
"position", List.of(g.getX(), g.getY(), g.getZ()))).toList());
|
||||
helper.assertTrue(!spawned.isEmpty(), "The native NaturalSpawner really creates at least one ordinary guardian inside the generated monument");
|
||||
for (var guardian : spawned) helper.assertTrue(guardian.isAlive() && level.getEntity(guardian.getUUID()) == guardian,
|
||||
"Naturally spawned ordinary guardian is a live server entity");
|
||||
} finally {
|
||||
spawned.forEach(Guardian::discard);
|
||||
level.getServer().getPlayerList().remove(player);
|
||||
}
|
||||
}
|
||||
void verifyLocator(StructureStart start) throws java.io.IOException {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var holder = registry.get(Identifier.parse("minecraft:monument")).orElseThrow();
|
||||
var wanted = registry.getOrThrow(StructureTags.ON_OCEAN_MONUMENT_MAPS);
|
||||
helper.assertTrue(wanted.contains(holder), "Vanilla ocean explorer map tag includes the monument");
|
||||
byte[] beforeJournal = Files.readAllBytes(journal);
|
||||
var beforeSession = List.copyOf(session.islands());
|
||||
var beforePublished = List.copyOf(generator.source().islands());
|
||||
int references = start.getReferences();
|
||||
boolean realReady = session.island(island.id()).status().equals("ready");
|
||||
BlockPos origin = start.getPieces().getFirst().getLocatorPosition();
|
||||
var found = generator.findNearestMapStructure(level, wanted, origin, 1, false);
|
||||
if (realReady) helper.assertTrue(found != null && found.getSecond().equals(holder) && found.getFirst().equals(origin),
|
||||
"Native explorer-map tag locates the actual persisted monument when its expansion is READY");
|
||||
else helper.assertTrue(found == null, "Native explorer-map tag cannot advertise an unfinished expansion");
|
||||
helper.assertTrue(start.getReferences() == references, "Ordinary tag search does not mark a map reference");
|
||||
if (!reload) {
|
||||
var firstMap = generator.findNearestMapStructure(level, wanted, origin, 1, true);
|
||||
if (realReady) {
|
||||
helper.assertTrue(firstMap != null && firstMap.getSecond().equals(holder) && firstMap.getFirst().equals(origin)
|
||||
&& start.getReferences() == references + 1, "Explorer-map tag references only its selected monument once");
|
||||
var startChunk = level.getChunk(start.getChunkPos().x(), start.getChunkPos().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
// The native read-only proto wrapper always reports isUnsaved=false,
|
||||
// although markUnsaved forwards to the real LevelChunk.
|
||||
var savedChunk = startChunk instanceof net.minecraft.world.level.chunk.ImposterProtoChunk wrapper
|
||||
? wrapper.getWrapped() : startChunk;
|
||||
report.put("referenced_start_chunk_view", startChunk.getClass().getSimpleName());
|
||||
report.put("referenced_start_chunk_storage", savedChunk.getClass().getSimpleName());
|
||||
helper.assertTrue(savedChunk.isUnsaved(), "The selected start chunk must save its new explorer-map reference even after READY");
|
||||
report.put("referenced_start_chunk_unsaved", true);
|
||||
var anotherMap = generator.findNearestMapStructure(level, wanted, origin, 1, true);
|
||||
helper.assertTrue(anotherMap == null || !anotherMap.getFirst().equals(origin), "skipReferenced never selects the same already referenced monument");
|
||||
helper.assertTrue(start.getReferences() == references + 1, "Searching for a second map leaves the first monument reference unchanged");
|
||||
} else helper.assertTrue(firstMap == null && start.getReferences() == references, "An unfinished expansion is neither located nor referenced");
|
||||
}
|
||||
var outside = new BlockPos(-20_000_000, 200, -20_000_000);
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, wanted, outside, 1, false) == null,
|
||||
"Searching beyond every admitted island cannot create an ocean or monument");
|
||||
helper.assertTrue(Arrays.equals(beforeJournal, Files.readAllBytes(journal)) && beforeSession.equals(session.islands())
|
||||
&& beforePublished.equals(generator.source().islands()), "Tag, explorer map and unsuccessful remote searches never create or activate an expansion");
|
||||
report.put("locator_on_real_ready", realReady); report.put("references", start.getReferences());
|
||||
report.put("locator", Map.of("tag", StructureTags.ON_OCEAN_MONUMENT_MAPS.location().toString(),
|
||||
"ready", realReady, "expected_position", List.of(origin.getX(), origin.getY(), origin.getZ()),
|
||||
"tag_search_verified", true, "remote_search_empty", true, "journal_unchanged", true, "islands_unchanged", true));
|
||||
}
|
||||
void release() { if (tickets) chunks.forEach(pos -> ChunkTickets25.remove(level, pos)); tickets = false; }
|
||||
void write(String name, String json) throws java.io.IOException { Path out = Path.of("diagnostics", "monument25"); Files.createDirectories(out); Files.writeString(out.resolve(name + ".json"), json); }
|
||||
void fail(Exception error) {
|
||||
if (tickets) recordReadiness();
|
||||
release(); report.put("passed", false); report.put("failure", error.toString()); report.put("progress", session.progress());
|
||||
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
|
||||
try { write("failure", new GsonBuilder().setPrettyPrinting().create().toJson(report)); } catch (Exception ignored) {}
|
||||
throw new IllegalStateException("Monument25 native lifecycle failed", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
/** Development-only counters around the actual vanilla fluid scheduler, never additional ticks. */
|
||||
public final class FluidTiming25 {
|
||||
private static long nanos, maximum, calls, executed;
|
||||
private static int pendingPeak;
|
||||
private FluidTiming25() {}
|
||||
public static synchronized void record(long elapsed, long count, int pending) {
|
||||
nanos += elapsed; maximum = Math.max(maximum, elapsed); calls++; executed += count;
|
||||
pendingPeak = Math.max(pendingPeak, pending);
|
||||
}
|
||||
public static synchronized java.util.Map<String, Object> snapshot() {
|
||||
return java.util.Map.of("scheduler_calls", calls, "executed_fluid_ticks", executed,
|
||||
"scheduler_total_ms", nanos / 1_000_000.0, "scheduler_max_ms", maximum / 1_000_000.0,
|
||||
"pending_peak", pendingPeak, "scope", "Actual vanilla Overworld fluid scheduler, development instrumentation only");
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Density;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Shape;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Disposable native world; reads scheduled vanilla fluid movement without scheduling extra ticks. */
|
||||
public final class Hydrology25WorldGameTests {
|
||||
private static final int[][] NEIGHBOURS = {{1,0,0},{-1,0,0},{0,0,1},{0,0,-1},{0,-1,0},{0,1,0}};
|
||||
private record BasinWitness(Terrain25Shape.Chamber chamber, List<BlockPos> surface, List<BlockPos> dryBanks) {}
|
||||
private record FlowWitness(int sources, int visited, int minimumFlowingY, List<Integer> lowest,
|
||||
boolean reachedFlank, boolean descended, boolean searchBoundReached,
|
||||
int minimumConnectedWaterY, List<Integer> lowestConnectedWater,
|
||||
boolean reachedY0, String observedExtent) {}
|
||||
|
||||
@GameTest(maxTicks = 16000)
|
||||
public void nativeRiverFallsAndRetainedBasins(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String,Object>();
|
||||
try { begin(helper, report); }
|
||||
catch (Exception error) { failure(report,error); throw error; }
|
||||
}
|
||||
|
||||
private static void begin(GameTestHelper helper, Map<String,Object> report) {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 25, "Hydrology witness requires a disposable generation25 world");
|
||||
int expected = Integer.getInteger("sanctuary.test.hydrology25.expectedFalls", -1);
|
||||
helper.assertTrue(expected >= 0 && expected <= 2, "Specify the expected 0, 1 or 2 river outlets for this witness");
|
||||
int observationTicks = Integer.getInteger("sanctuary.test.hydrology25.observationTicks",600);
|
||||
helper.assertTrue(observationTicks >= 600 && observationTicks <= 3600,
|
||||
"Hydrology observation must be between600 and3600 native game ticks");
|
||||
var starter = generator.starterContext();
|
||||
var capacity = Objects.requireNonNull(IslandCapacity.forGenerator(starter.generator()));
|
||||
var basins = Terrain25Runtime.admittedLakes(starter.generator(),starter.random());
|
||||
var density = Terrain25Runtime.view(starter.generator(),starter.random());
|
||||
var falls = new LinkedHashSet<Terrain25Runtime.Fall>();
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
report.put("seed",level.getSeed()); report.put("diameter",generator.initialDiameter());
|
||||
report.put("expected_falls",expected); report.put("process_id",ProcessHandle.current().pid());
|
||||
report.put("requested_observation_ticks",observationTicks);
|
||||
var first = capacity.regionAt(-capacity.terrainLimit(),-capacity.terrainLimit());
|
||||
var last = capacity.regionAt(capacity.terrainLimit(),capacity.terrainLimit());
|
||||
for (int rx=first.x();rx<=last.x();rx++) for (int rz=first.z();rz<=last.z();rz++) {
|
||||
var region = new IslandCapacity.Region(rx,rz);
|
||||
double nx=Math.max(0,Math.max(region.minX(),-region.maxX()));
|
||||
double nz=Math.max(0,Math.max(region.minZ(),-region.maxZ()));
|
||||
if (Math.hypot(nx,nz)>=capacity.terrainLimit()) continue;
|
||||
falls.addAll(Terrain25Runtime.falls(starter.generator(),starter.random(),region));
|
||||
var water=PopulationHydrologyRuntime.regionPlan(starter.generator(),starter.random(),region).water();
|
||||
// Complete wet footprints, not just a lake's centre chunk. Current FULL chunks are read,
|
||||
// never reset or regenerated. Chunk tickets additionally cover their simulation neighbours.
|
||||
for (var cell:water.cells()) if(cell.hasWater()) addBounds(chunks,cell.x()-1,cell.z()-1,cell.x()+1,cell.z()+1);
|
||||
for (var spring:water.springs()) {
|
||||
var b=spring.flowBounds();addBounds(chunks,b.minX(),b.minZ(),b.maxX(),b.maxZ());
|
||||
}
|
||||
for (var spill:water.spills()) {
|
||||
var b=spill.flowBounds();addBounds(chunks,b.minX(),b.minZ(),b.maxX(),b.maxZ());
|
||||
}
|
||||
}
|
||||
helper.assertTrue(falls.size()==expected,"Immutable river plan has exactly the expected number of suitable exits: "+falls.size());
|
||||
report.put("fall_plan",List.copyOf(falls)); report.put("admitted_basins",basins);
|
||||
helper.assertTrue(basins.stream().anyMatch(c->c.kind()==Terrain25Shape.Kind.LAKE),"A retained water lake must actually be present in the witness");
|
||||
if (expected>0) helper.assertTrue(basins.stream().anyMatch(c->c.kind()==Terrain25Shape.Kind.LAVA),
|
||||
"The flowing witnesses must also contain an admitted lava lake for physical separation checks");
|
||||
for (var basin:basins) addBounds(chunks,(int)Math.floor(basin.x()-basin.rx()-8),(int)Math.floor(basin.z()-basin.rz()-8),
|
||||
(int)Math.ceil(basin.x()+basin.rx()+8),(int)Math.ceil(basin.z()+basin.rz()+8));
|
||||
for (var fall:falls) addBounds(chunks,fall.minX()-7,fall.minZ()-7,fall.maxX()+7,fall.maxZ()+7);
|
||||
report.put("requested_full_chunks",chunks.size());
|
||||
var iterator=chunks.iterator();
|
||||
var witnesses=new ArrayList<BasinWitness>();
|
||||
var flowHistory=new ArrayList<Map<String,Object>>(); report.put("flow_history",flowHistory);
|
||||
long started=System.nanoTime(); long[] fullNanos={0}; int[] fullCalls={0},wait={0};
|
||||
long[] tickingStart={-1}; boolean[] ticketed={false};
|
||||
Map<String,Object> schedulerStart=FluidTiming25.snapshot(); report.put("scheduler_before",schedulerStart);
|
||||
Runnable release=()->{
|
||||
if(ticketed[0])chunks.forEach(cp->ChunkTickets25.remove(level, cp));
|
||||
ticketed[0]=false;
|
||||
};
|
||||
Runnable[] step=new Runnable[1];
|
||||
step[0]=()->{
|
||||
try {
|
||||
if(iterator.hasNext()) {
|
||||
var cp=iterator.next();report.put("stage","FULL wet footprint "+cp);
|
||||
long start=System.nanoTime();level.getChunk(cp.x(),cp.z(),ChunkStatus.FULL,true);
|
||||
fullNanos[0]+=System.nanoTime()-start;fullCalls[0]++;
|
||||
helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;
|
||||
}
|
||||
if(!ticketed[0]) {
|
||||
chunks.forEach(cp->ChunkTickets25.add(level, cp));ticketed[0]=true;
|
||||
}
|
||||
boolean ready=chunks.stream().allMatch(cp->level.areEntitiesLoaded(cp.pack())&&level.areEntitiesActuallyLoadedAndTicking(cp));
|
||||
if(!ready) {
|
||||
report.put("stage","wait for native fluid simulation chunks");report.put("readiness_wait_ticks",wait[0]);
|
||||
helper.assertTrue(wait[0]++<6000,"All wet footprints and fall columns become natively loaded and ticking");
|
||||
helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;
|
||||
}
|
||||
if(tickingStart[0]<0) {
|
||||
tickingStart[0]=helper.getTick();report.put("stage","record retained waterlines before observation");
|
||||
for(var basin:basins)witnesses.add(captureBasin(helper,level,basin));
|
||||
report.put("basins_before",basinState(helper,level,witnesses,false));
|
||||
report.put("full_get_chunk_calls",fullCalls[0]);report.put("full_get_chunk_ms",fullNanos[0]/1_000_000);
|
||||
report.put("full_loop_elapsed_ms",(System.nanoTime()-started)/1_000_000);
|
||||
}
|
||||
long elapsed=helper.getTick()-tickingStart[0];report.put("observation_ticks",elapsed);
|
||||
report.put("stage","observe unchanged native fluid scheduler");
|
||||
var flows=falls.stream().map(fall->Map.of("fall",fall,"actual_flow",traceFall(level,density,fall,false))).toList();
|
||||
flowHistory.add(Map.of("tick",helper.getTick(),"flows",flows));
|
||||
boolean flowing=flows.stream().allMatch(row->((FlowWitness)row.get("actual_flow")).descended());
|
||||
if(elapsed<observationTicks||!flowing) {
|
||||
helper.assertTrue(elapsed<6000,"Every admitted river outlet produces connected flowing water down at least16 blocks, or reaches a natural flank after6");
|
||||
helper.runAtTickTime(helper.getTick()+100,()->step[0].run());return;
|
||||
}
|
||||
report.put("basins_after",basinState(helper,level,witnesses,true));
|
||||
// Unlike the admission trace, the final read does not stop after16 blocks.
|
||||
// A natural flank is a valid observation, not an obligation to reach the void.
|
||||
report.put("final_flow_extents",falls.stream().map(fall->Map.of("fall",fall,
|
||||
"actual_flow",traceFall(level,density,fall,true))).toList());
|
||||
report.put("final_flow_extent_scope","Lowest connected water observed at the end inside each outlet's existing X/Z footprint+7, Y0..waterY; horizontal/downward traversal only, at most120000 cells. A natural-flank result does not imply the flow has permanently stopped. No Y0 requirement.");
|
||||
Map<String,Object> schedulerEnd=FluidTiming25.snapshot();report.put("scheduler_after",schedulerEnd);
|
||||
long executed=((Number)schedulerEnd.get("executed_fluid_ticks")).longValue()-((Number)schedulerStart.get("executed_fluid_ticks")).longValue();
|
||||
report.put("executed_native_fluid_ticks",executed);
|
||||
helper.assertTrue(executed>0,"Actual native fluid ticks were observed; no synthetic scheduler or manual spread");
|
||||
report.put("observed_server_ticks",helper.getTick());report.put("elapsed_ms",(System.nanoTime()-started)/1_000_000);
|
||||
report.put("heap_used",Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
|
||||
report.put("passed",true);write(report,"full.json");release.run();helper.succeed();
|
||||
} catch(Exception error) {
|
||||
release.run();failure(report,error);throw new IllegalStateException("Hydrology25 failed: "+error.getMessage(),error);
|
||||
}
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick()+1,()->step[0].run());
|
||||
}
|
||||
|
||||
private static BasinWitness captureBasin(GameTestHelper helper,ServerLevel level,Terrain25Shape.Chamber chamber) {
|
||||
var surface=new ArrayList<BlockPos>();var banks=new ArrayList<BlockPos>();
|
||||
int minX=(int)Math.floor(chamber.x()-chamber.rx()-3),maxX=(int)Math.ceil(chamber.x()+chamber.rx()+3);
|
||||
int minZ=(int)Math.floor(chamber.z()-chamber.rz()-3),maxZ=(int)Math.ceil(chamber.z()+chamber.rz()+3);
|
||||
for(int x=minX;x<=maxX;x++)for(int z=minZ;z<=maxZ;z++) {
|
||||
var pos=new BlockPos(x,chamber.fluidY(),z);
|
||||
// The immutable plan defines the baseline, including cells already missing at FULL.
|
||||
// Filtering by present fluid here would hide losses before the observation starts.
|
||||
if(chamber.cut(x,chamber.fluidY(),z)>0) surface.add(pos);
|
||||
if(banks.size()>=64||Math.floorMod(x+z,3)!=0||chamber.horizontal(x,z)<.55||chamber.horizontal(x,z)>1.2)continue;
|
||||
// A dry, walkable real bank; no artificial platform or fluid alteration in the test.
|
||||
for(int y=chamber.fluidY();y<=chamber.fluidY()+8;y++) {
|
||||
var floor=new BlockPos(x,y,z);
|
||||
if(chamber.cut(x,y,z)>0||!level.getBlockState(floor).isSolid()||!level.getBlockState(floor.above()).isAir()
|
||||
||!level.getBlockState(floor.above(2)).isAir())continue;
|
||||
if(level.getFluidState(floor.above()).isEmpty()&&level.getFluidState(floor.above(2)).isEmpty()) {banks.add(floor);break;}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(surface.size()>=50,"Admitted "+chamber.kind()+" basin "+chamber.id()+" plans at least50 surface fluid blocks, got "+surface.size());
|
||||
helper.assertTrue(!banks.isEmpty(),"Admitted basin "+chamber.id()+" has a real accessible dry bank");
|
||||
return new BasinWitness(chamber,List.copyOf(surface),List.copyOf(banks));
|
||||
}
|
||||
|
||||
private static List<Map<String,Object>> basinState(GameTestHelper helper,ServerLevel level,List<BasinWitness> witnesses,boolean after) {
|
||||
var rows=new ArrayList<Map<String,Object>>();
|
||||
for(var witness:witnesses) {
|
||||
var c=witness.chamber();int retained=0,supported=0,dry=0;
|
||||
for(var pos:witness.surface()) {
|
||||
if(matches(level,pos,c.kind()))retained++;
|
||||
if(level.getBlockState(new BlockPos(pos.getX(),c.floorY(),pos.getZ())).isSolid()
|
||||
&&level.getBlockState(new BlockPos(pos.getX(),c.floorY()-1,pos.getZ())).isSolid())supported++;
|
||||
}
|
||||
for(var pos:witness.dryBanks()) if(level.getBlockState(pos).isSolid()
|
||||
&&level.getFluidState(pos.above()).isEmpty()&&level.getFluidState(pos.above(2)).isEmpty())dry++;
|
||||
var contact=unexpectedContact(level,c);
|
||||
rows.add(Map.of("id",c.id(),"kind",c.kind(),"baseline_surface",witness.surface().size(),"retained_surface",retained,
|
||||
"supported_columns",supported,"dry_banks",dry,"baseline_dry_banks",witness.dryBanks().size(),"unexpected_contact",contact));
|
||||
helper.assertTrue(contact.isEmpty(),"Water and lava remain separated around admitted basin "+c.id()+": "+contact);
|
||||
helper.assertTrue(supported==witness.surface().size(),"Natural lake floor remains continuous at two depths: "+c.id());
|
||||
helper.assertTrue(retained==witness.surface().size(),(after?"After vanilla fluid ticks":"Before observation")+", retained lake waterline is intact: "+c.id());
|
||||
helper.assertTrue(dry==witness.dryBanks().size(),"The observed dry banks remain solid and free of leaking fluids: "+c.id());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Integer> unexpectedContact(ServerLevel level,Terrain25Shape.Chamber c) {
|
||||
for(int x=(int)(c.x()-c.rx()-3);x<=c.x()+c.rx()+3;x++)for(int z=(int)(c.z()-c.rz()-3);z<=c.z()+c.rz()+3;z++) {
|
||||
if(c.horizontal(x,z)>1.3)continue;
|
||||
for(int y=c.floorY();y<=c.fluidY()+3;y++) {
|
||||
var pos=new BlockPos(x,y,z);var fluid=level.getFluidState(pos);
|
||||
if(!fluid.is(FluidTags.WATER)&&!fluid.is(FluidTags.LAVA))continue;
|
||||
for(var d:NEIGHBOURS) {
|
||||
var other=level.getFluidState(pos.offset(d[0],d[1],d[2]));
|
||||
if(fluid.is(FluidTags.WATER)&&other.is(FluidTags.LAVA)||fluid.is(FluidTags.LAVA)&&other.is(FluidTags.WATER))return coords(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private static FlowWitness traceFall(ServerLevel level,Terrain25Density.View density,Terrain25Runtime.Fall fall,boolean fullExtent) {
|
||||
var queue=new ArrayDeque<BlockPos>();var seen=new HashSet<BlockPos>();
|
||||
int sources=0,minY=fall.waterY(),minConnectedY=fall.waterY();
|
||||
BlockPos lowest=null,lowestConnected=null;boolean flank=false;
|
||||
for(int x=fall.minX();x<=fall.maxX();x++)for(int z=fall.minZ();z<=fall.maxZ();z++) {
|
||||
if(!fall.channel(x,z)||fall.progress(x,z)<.55||fall.progress(x,z)>.95)continue;
|
||||
for(int y=fall.waterY()-2;y<=fall.waterY();y++) {
|
||||
var p=new BlockPos(x,y,z);var f=level.getFluidState(p);
|
||||
if(f.is(FluidTags.WATER)&&seen.add(p)){queue.addLast(p);if(f.isSource())sources++;}
|
||||
}
|
||||
}
|
||||
// A read-only connectivity search proves the low flowing water belongs to this outlet.
|
||||
// Only horizontal/downstream neighbours are traversed; an unrelated upper pool cannot count.
|
||||
while(!queue.isEmpty()&&seen.size()<120_000) {
|
||||
var pos=queue.removeFirst();var fluid=level.getFluidState(pos);
|
||||
if(lowestConnected==null||pos.getY()<minConnectedY){minConnectedY=pos.getY();lowestConnected=pos;}
|
||||
if(!fluid.isSource()&&pos.getY()<minY) {minY=pos.getY();lowest=pos;}
|
||||
if(!fluid.isSource()&&pos.getY()<=fall.waterY()-6&&level.getBlockState(pos.below()).isSolid()
|
||||
&&density.natural(SamplerContext.EMPTY_UNCACHED,pos.getX(),pos.getY()-1,pos.getZ())>0)flank=true;
|
||||
if(!fullExtent&&minY<=fall.waterY()-16)break;
|
||||
for(int i=0;i<5;i++) {
|
||||
var d=NEIGHBOURS[i];var n=pos.offset(d[0],d[1],d[2]);
|
||||
if(n.getX()<fall.minX()-7||n.getX()>fall.maxX()+7||n.getZ()<fall.minZ()-7||n.getZ()>fall.maxZ()+7
|
||||
||n.getY()<0||n.getY()>fall.waterY()||fall.progress(n.getX(),n.getZ())<.45
|
||||
||seen.contains(n)||!level.getFluidState(n).is(FluidTags.WATER))continue;
|
||||
seen.add(n);queue.addLast(n);
|
||||
}
|
||||
}
|
||||
boolean y0=lowestConnected!=null&&minConnectedY==0;
|
||||
String extent=y0?"water_reached_y0":flank?"water_observed_on_natural_flank_above_y0"
|
||||
:minY<=fall.waterY()-16?"descending_above_y0":"no_confirmed_descent";
|
||||
return new FlowWitness(sources,seen.size(),minY,lowest==null?List.of():coords(lowest),flank,
|
||||
minY<=fall.waterY()-16||flank,seen.size()>=120_000,
|
||||
lowestConnected==null?-1:minConnectedY,lowestConnected==null?List.of():coords(lowestConnected),y0,extent);
|
||||
}
|
||||
|
||||
private static boolean matches(ServerLevel level,BlockPos pos,Terrain25Shape.Kind kind) {
|
||||
return level.getFluidState(pos).is(kind==Terrain25Shape.Kind.LAVA?FluidTags.LAVA:FluidTags.WATER);
|
||||
}
|
||||
private static void addBounds(Set<ChunkPos> target,int minX,int minZ,int maxX,int maxZ) {
|
||||
for(int cx=minX>>4;cx<=maxX>>4;cx++)for(int cz=minZ>>4;cz<=maxZ>>4;cz++)target.add(new ChunkPos(cx,cz));
|
||||
}
|
||||
private static List<Integer> coords(BlockPos pos) {return List.of(pos.getX(),pos.getY(),pos.getZ());}
|
||||
private static void write(Map<String,Object> report,String file) throws java.io.IOException {
|
||||
var directory=Path.of("diagnostics/hydrology25",report.get("diameter")+"-"+report.get("seed"));
|
||||
Files.createDirectories(directory);Files.writeString(directory.resolve(file),new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
}
|
||||
private static void failure(Map<String,Object> report,Exception error) {
|
||||
report.put("passed",false);report.put("failure",error.toString());report.put("scheduler_at_failure",FluidTiming25.snapshot());
|
||||
var stack=new java.io.StringWriter();error.printStackTrace(new java.io.PrintWriter(stack));report.put("stacktrace",stack.toString());
|
||||
SanctuaryMod.LOGGER.error("Hydrology25 failed at {}",report.get("stage"),error);
|
||||
try {write(report,"failure.json");}catch(java.io.IOException failed){error.addSuppressed(failed);}
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPiece23;
|
||||
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23;
|
||||
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryRenderer25;
|
||||
import fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Native block and NBT checks for an accepted vault; never turns conditional absence into presence evidence. */
|
||||
public final class Mineral25NativeChecks {
|
||||
private Mineral25NativeChecks() {}
|
||||
|
||||
public static void verify(GameTestHelper helper, Map<String, Object> report) {
|
||||
var level = helper.getLevel();
|
||||
helper.assertTrue(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host
|
||||
&& host.rootGeneration() == 25, "Mineral25 checks require the actual generation25 world");
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var plan = generator.mineralSanctuary23();
|
||||
var evidence = new LinkedHashMap<String, Object>();
|
||||
report.put("mineral_sanctuary25", evidence);
|
||||
evidence.put("presence_verified", false); evidence.put("plan_diagnostics", plan.diagnostic());
|
||||
evidence.put("plan_columns", plan.columns()); evidence.put("plan_samples", plan.samples());
|
||||
var biomeRegistry = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
int tagMemberships = 0;
|
||||
for (String family : List.of("forgotten_networks", "heritage_networks")) {
|
||||
var key = net.minecraft.tags.TagKey.create(Registries.BIOME,
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("sanctuary", "has_structure/" + family));
|
||||
var allowed = biomeRegistry.getOrThrow(key);
|
||||
for (String name : fr.koka.sanctuary.worldgen.Terrain25Biomes.NAMES) {
|
||||
var biome = biomeRegistry.getOrThrow(fr.koka.sanctuary.worldgen.Terrain25Biomes.key(name));
|
||||
helper.assertTrue(allowed.contains(biome), "Every generation25 clone remains eligible for " + family + ": " + name);
|
||||
tagMemberships++;
|
||||
}
|
||||
}
|
||||
evidence.put("terrain25_structure_tag_memberships_verified", tagMemberships);
|
||||
if (plan.isEmpty()) {
|
||||
evidence.put("status", "not_admitted");
|
||||
evidence.put("reason", !level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()
|
||||
? "structures_disabled" : "no_suitable_candidate");
|
||||
return;
|
||||
}
|
||||
evidence.put("status", "checking_accepted_plan");
|
||||
var c = plan.chamber();
|
||||
evidence.put("chamber", List.of(c.minX(), c.minY(), c.minZ(), c.width(), c.height(), c.depth(), c.facing()));
|
||||
var chunks = c.bounds().intersectingChunks().toList();
|
||||
for (var pos : chunks) helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Accepted mineral geometry is checked in genuinely FULL chunks: " + pos);
|
||||
evidence.put("full_chunks", chunks.size());
|
||||
|
||||
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(MineralSanctuaryStructures23.KEY).value();
|
||||
var sourceChunk = level.getChunk(c.startChunkX(), c.startChunkZ());
|
||||
var start = sourceChunk.getStartForStructure(type);
|
||||
evidence.put("start_chunk", List.of(c.startChunkX(), c.startChunkZ()));
|
||||
evidence.put("native_start_present", start != null);
|
||||
evidence.put("native_start_valid", start != null && start.isValid());
|
||||
evidence.put("native_piece_count", start == null ? 0 : start.getPieces().size());
|
||||
evidence.put("starts_in_chunk", sourceChunk.getAllStarts().entrySet().stream().map(entry -> Map.of(
|
||||
"id", level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(entry.getKey()).toString(),
|
||||
"valid", entry.getValue().isValid(), "pieces", entry.getValue().getPieces().size())).toList());
|
||||
var atEntrance = level.getBiome(c.locator());
|
||||
evidence.put("entrance_biome", atEntrance.unwrapKey().orElseThrow().identifier().toString());
|
||||
evidence.put("entrance_biome_in_structure_tag", type.biomes().contains(atEntrance));
|
||||
helper.assertTrue(start != null && start.isValid() && start.getPieces().size() == 1,
|
||||
"An accepted mineral vault has its single native saved piece");
|
||||
helper.assertTrue(start.getPieces().getFirst() instanceof MineralSanctuaryPiece23 mineral && mineral.chamber().equals(c),
|
||||
"Native mineral piece exactly matches the accepted immutable geometry");
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var tag = start.createTag(context, start.getChunkPos());
|
||||
var pieceTag = tag.getList("Children").orElseThrow().getCompound(0).orElseThrow();
|
||||
helper.assertTrue(pieceTag.getInt("SanctuaryMineralRenderer").orElse(-1) == 25,
|
||||
"The actually generated piece persists the renderer25 discriminator");
|
||||
var decoded = StructureStart.loadStaticStart(context, tag.copy(), level.getSeed());
|
||||
helper.assertTrue(decoded != null && decoded.isValid() && tag.equals(decoded.createTag(context, decoded.getChunkPos())),
|
||||
"The complete native mineral start survives NBT read/write exactly, without replanning");
|
||||
helper.assertTrue(decoded.getPieces().getFirst() instanceof MineralSanctuaryPiece23 mineral && mineral.chamber().equals(c),
|
||||
"NBT re-reading retains the exact mineral chamber");
|
||||
evidence.put("native_tag", tag.toString()); evidence.put("nbt_round_trip", true);
|
||||
|
||||
int bottom = c.floor(c.width() / 2, c.depth() / 2);
|
||||
int frame = 0, aperture = 0, accessAir = 0;
|
||||
for (int lateral = -3; lateral <= 3; lateral++) for (int y = bottom; y <= bottom + 8; y++) {
|
||||
var pos = portalPosition(c, lateral, 0, y);
|
||||
boolean border = Math.abs(lateral) == 3 || y == bottom || y == bottom + 8;
|
||||
var actual = level.getBlockState(pos);
|
||||
helper.assertTrue(border ? actual.is(Blocks.BLACKSTONE) : actual.isAir(),
|
||||
"The real central frame is 7x9 blackstone around an inactive 5x7 opening: " + pos + "=" + actual);
|
||||
if (border) frame++; else aperture++;
|
||||
}
|
||||
helper.assertTrue(frame == 28 && aperture == 35, "Exact frame and aperture block counts");
|
||||
for (int lateral = -2; lateral <= 2; lateral++) for (int normal = -2; normal <= 2; normal++) {
|
||||
var ground = portalPosition(c, lateral, normal, bottom);
|
||||
helper.assertTrue(level.getBlockState(ground).isCollisionShapeFullBlock(level, ground),
|
||||
"Both sides of the inactive frame have continuous usable ground: " + ground);
|
||||
for (int y = bottom + 1; y <= bottom + 7; y++) {
|
||||
var pos = portalPosition(c, lateral, normal, y);
|
||||
helper.assertTrue(level.getBlockState(pos).isAir(), "The opening and approaches on both sides stay clear: " + pos);
|
||||
accessAir++;
|
||||
}
|
||||
}
|
||||
Map<BlockPos, BlockState> expectedStairs = new HashMap<>();
|
||||
MineralSanctuaryRenderer25.render(c, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
if (state.getBlock() instanceof StairBlock) expectedStairs.put(new BlockPos(c.minX() + x, c.minY() + y, c.minZ() + z), state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
throw new AssertionError("The mineral sanctuary has natural veins, not loot containers");
|
||||
}
|
||||
});
|
||||
helper.assertTrue(!expectedStairs.isEmpty(), "The accepted vault includes actual stone terraces with stairs");
|
||||
var shapes = new TreeMap<String, Integer>();
|
||||
for (var entry : expectedStairs.entrySet()) {
|
||||
var pos = entry.getKey(); var actual = level.getBlockState(pos);
|
||||
helper.assertTrue(actual.equals(entry.getValue()), "Native placement retains the resolved stair orientation and corner: " + pos + "=" + actual);
|
||||
helper.assertTrue(actual.is(Blocks.STONE_STAIRS) || actual.is(Blocks.COBBLESTONE_STAIRS), "Mineral stair palette");
|
||||
helper.assertTrue(level.getBlockState(pos.below()).isCollisionShapeFullBlock(level, pos.below()), "A placed stair has solid natural support: " + pos);
|
||||
shapes.merge(actual.getValue(StairBlock.SHAPE).getSerializedName(), 1, Integer::sum);
|
||||
}
|
||||
int portalBlocks = 0;
|
||||
for (var pos : BlockPos.betweenClosed(c.minX(), c.minY(), c.minZ(), c.maxX(), c.maxY(), c.maxZ()))
|
||||
if (level.getBlockState(pos).is(Blocks.NETHER_PORTAL)) portalBlocks++;
|
||||
helper.assertTrue(portalBlocks == 0, "The mineral vault does not activate any Nether portal");
|
||||
evidence.put("frame_blocks", frame); evidence.put("aperture_air_blocks", aperture);
|
||||
evidence.put("clear_access_air_blocks", accessAir); evidence.put("actual_stairs", expectedStairs.size());
|
||||
evidence.put("actual_stair_shapes", shapes); evidence.put("nether_portal_blocks", portalBlocks);
|
||||
evidence.put("status", "present_verified"); evidence.put("presence_verified", true);
|
||||
}
|
||||
|
||||
private static BlockPos portalPosition(MineralSanctuaryPlan23.Chamber c, int lateral, int normal, int y) {
|
||||
return new BlockPos(c.centerX() + (c.facing() % 2 == 0 ? lateral : normal),
|
||||
c.minY() + y, c.centerZ() + (c.facing() % 2 == 0 ? normal : lateral));
|
||||
}
|
||||
}
|
||||
+66
-3
@@ -57,16 +57,21 @@ public final class PopulationDiagnostics {
|
||||
"New trees must refuse older saved worlds");
|
||||
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||
"New cave decorations must refuse older saved worlds");
|
||||
helper.assertFalse(Terrain25Runtime.enabled(old), "Terrain25 cannot attach to a historical settings key: " + key);
|
||||
}
|
||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
|
||||
var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES);
|
||||
if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city");
|
||||
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
|
||||
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24 || unified.rootGeneration() == 25) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
|
||||
.forEach(name -> expected.add("minecraft:" + name));
|
||||
if (unified.rootGeneration() == 25) Terrain25Biomes.NAMES
|
||||
.forEach(name -> expected.add(Terrain25Biomes.key(name).identifier().toString()));
|
||||
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
|
||||
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
|
||||
.collect(java.util.stream.Collectors.toSet()).equals(expected),
|
||||
"Historical holders stay intact; generations23/24 add the expansion ocean and shore palette");
|
||||
"The exact saved biome catalogue belongs to the active generation; historical holders stay intact");
|
||||
helper.assertTrue(Terrain25Runtime.enabled(generator) == (unified.rootGeneration() == 25),
|
||||
"Only generation25 activates the new terrain/hydrology pass");
|
||||
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
|
||||
"The unified starter must use its real terrain state, never the expansion host fallback");
|
||||
} else {
|
||||
@@ -82,6 +87,7 @@ public final class PopulationDiagnostics {
|
||||
var level = helper.getLevel();
|
||||
var binding = binding(level);
|
||||
var generator = binding.generator();
|
||||
if (Terrain25Runtime.enabled(generator)) { verifyDensity25(helper, binding); return; }
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
var density = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
|
||||
@@ -146,11 +152,68 @@ public final class PopulationDiagnostics {
|
||||
capacity.players(), level.getSeed(), solidColumns, outerColumns);
|
||||
}
|
||||
|
||||
private static void verifyDensity25(GameTestHelper helper, PopulationGenerationContext.Binding binding) throws IOException {
|
||||
var level = helper.getLevel(); var generator = binding.generator(); var random = binding.random();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
var registered = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("terrain25_" + capacity.players()))).value();
|
||||
helper.assertTrue(registered instanceof Terrain25Density terrain && terrain.capacity() == capacity,
|
||||
"Terrain25 settings and its named density agree on saved capacity");
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var view = Terrain25Runtime.view(generator, random);
|
||||
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
|
||||
helper.assertTrue(view.plan().equals(Terrain25Runtime.view(generator, replay).plan()),
|
||||
"Fresh native random states reproduce the same immutable terrain25 plan");
|
||||
int step = Math.max(8, (int) Math.ceil(capacity.terrainLimit() / 32.0));
|
||||
int extent = (int) Math.ceil(capacity.terrainLimit() / (double) step) * step;
|
||||
int solidColumns = 0, emptyColumns = 0, replayed = 0, cutSamples = 0;
|
||||
Set<Integer> bottoms = new TreeSet<>();
|
||||
StringBuilder map = new StringBuilder("x,z,bottom_y,top_y\n");
|
||||
for (int x = -extent; x <= extent; x += step) for (int z = -extent; z <= extent; z += step) {
|
||||
int bottom = -1, top = -1;
|
||||
for (int y = 0; y < 384; y += 8) {
|
||||
float value = random.sampleBlockValueUncached(density, x, y, z);
|
||||
float intact = view.natural(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.EMPTY_UNCACHED, x, y, z);
|
||||
helper.assertTrue(Float.isFinite(value) && Float.isFinite(intact), "Terrain25 samples remain finite");
|
||||
helper.assertTrue(value <= 0 || intact > 0, "Cavities remove intact rock rather than add artificial masses");
|
||||
if (y > Terrain25Shape.MAX_TERRAIN_Y || y == 0 || Math.hypot(x, z) >= capacity.terrainLimit())
|
||||
helper.assertTrue(value <= 0, "Terrain25 respects its finite footprint and vertical bounds");
|
||||
if (intact > 0 && value <= 0) cutSamples++;
|
||||
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
|
||||
if (x % (step * 8) == 0 && z % (step * 8) == 0) {
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
|
||||
"Actual terrain25 density is bit-identical in a fresh native random state");
|
||||
replayed++;
|
||||
}
|
||||
}
|
||||
if (top >= 0) { solidColumns++; bottoms.add(bottom); } else emptyColumns++;
|
||||
map.append(x).append(',').append(z).append(',').append(bottom).append(',').append(top).append('\n');
|
||||
}
|
||||
helper.assertTrue(solidColumns > 100 && emptyColumns > 100 && bottoms.size() >= 4 && cutSamples > 0 && replayed > 100,
|
||||
"The actual terrain25 has natural masses, void, varied underside and sampled cave cuts");
|
||||
Path directory = Path.of("diagnostics"); Files.createDirectories(directory);
|
||||
String prefix = "terrain25-" + capacity.players() + "-seed-" + level.getSeed();
|
||||
Files.writeString(directory.resolve(prefix + "-density.csv"), map.toString());
|
||||
Files.writeString(directory.resolve(prefix + "-density.json"), json(Map.of(
|
||||
"generation", 25, "players", capacity.players(), "seed", level.getSeed(), "step", step,
|
||||
"solid_columns", solidColumns, "empty_columns", emptyColumns, "underside_heights", bottoms,
|
||||
"cave_cut_samples", cutSamples, "bit_exact_replayed_samples", replayed, "passed", true)));
|
||||
}
|
||||
|
||||
static void verifyExterior(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var capacity = IslandCapacity.forGenerator(binding(level).generator());
|
||||
int edge = Math.floorDiv(capacity.terrainLimit() + 48, 16) + 1;
|
||||
for (int[] point : new int[][]{{edge,0},{-edge-1,0},{0,edge},{0,-edge-1},{256,0},{-257,0}}) {
|
||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host && host.rootGeneration() == 25) {
|
||||
var initialFlows = fr.koka.sanctuary.expansion.ExpansionInitialFlows25.prepare(host);
|
||||
int attempts = 0;
|
||||
while (host.aerialPlan24().claimsChunks().contains(ChunkPos.pack(point[0], point[1]))
|
||||
|| initialFlows.contains(ChunkPos.pack(point[0], point[1]))) {
|
||||
helper.assertTrue(++attempts <= 64, "A finite reservation must leave an exterior void witness");
|
||||
if (point[0] != 0) point[0] += Integer.signum(point[0]); else point[1] += Integer.signum(point[1]);
|
||||
}
|
||||
}
|
||||
var chunk = full(helper, level, point[0], point[1]);
|
||||
for (var section : chunk.getSections()) helper.assertTrue(section.hasOnlyAir(),
|
||||
"Completed terrain and decoration must remain void beyond this capacity: " + chunk.getPos());
|
||||
@@ -215,7 +278,7 @@ public final class PopulationDiagnostics {
|
||||
|
||||
void prepare() throws IOException {
|
||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
|
||||
&& (sanctuary.rootGeneration() == 25 || sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
|
||||
// Generation19 explicitly prospects destinations across the root island before publishing them.
|
||||
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||
// Historical generators retain their original near-spawn contract.
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Registry-backed diagnostics of nine plans; no evaluated world chunks are loaded or modified. */
|
||||
public final class Terrain25Diagnostics {
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void nineInitialTerrainPlans(GameTestHelper helper) throws Exception {
|
||||
var registries=helper.getLevel().registryAccess();
|
||||
var settings=registries.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
var noises=registries.lookupOrThrow(Registries.NOISE);
|
||||
var forest=registries.lookupOrThrow(Registries.BIOME).getOrThrow(Terrain25Biomes.key("oak_forest"));
|
||||
List<Map<String,Object>> reports=new ArrayList<>();
|
||||
int acceptedWater=0,acceptedLava=0,totalFalls=0;
|
||||
Path reportPath=Path.of("diagnostics/terrain25/plans.json"); Files.createDirectories(reportPath.getParent());
|
||||
for(int players:new int[]{5,10,20})for(long seed:new long[]{0,42,2026}) {
|
||||
long start=System.nanoTime();
|
||||
var holder=settings.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS,SanctuaryMod.id("unified_25_"+players)));
|
||||
var generator=new NoiseBasedChunkGenerator(new FixedBiomeSource(forest),holder);
|
||||
var state=RandomState.create(noises,seed,holder.value());
|
||||
var view=Terrain25Runtime.view(generator,state); var plan=view.plan();
|
||||
var capacity=IslandCapacity.fromPlayers(players);
|
||||
Map<String,Object> row=new LinkedHashMap<>(); row.put("seed",seed);row.put("diameter",players==5?512:players==10?724:1024);
|
||||
int max=-1,columns=0,raised=0,vegetationRoom=0,solidColumns=0; long digest=0xcbf29ce484222325L;
|
||||
var context=SamplerContext.builder().enableCaches().build();
|
||||
for(int x=-capacity.terrainLimit();x<=capacity.terrainLimit();x+=16)for(int z=-capacity.terrainLimit();z<=capacity.terrainLimit();z+=16) {
|
||||
if(Math.hypot(x,z)>=capacity.radius()*.82)continue;
|
||||
columns++;if(plan.lift(x,z)>0)raised++;
|
||||
int top=top(view,context,x,z);max=Math.max(max,top);
|
||||
if(top>=0){solidColumns++;if(top<335)vegetationRoom++;}
|
||||
for(int y:new int[]{0,32,80,120,151,192,256,300,358,359,383}) {
|
||||
float density=view.sampleValue(context,x,y,z);digest=(digest^Float.floatToIntBits(density))*0x100000001b3L;
|
||||
if(y>358)helper.assertTrue(density<=0,"Terrain stays below the immutable build-height margin");
|
||||
}
|
||||
}
|
||||
for(var peak:plan.peaks())max=Math.max(max,top(view,context,(int)Math.round(peak.x()),(int)Math.round(peak.z())));
|
||||
var lakes=Terrain25Runtime.admittedLakes(generator,state);
|
||||
long water=lakes.stream().filter(c->c.kind()==Terrain25Shape.Kind.LAKE).count();
|
||||
long lava=lakes.stream().filter(c->c.kind()==Terrain25Shape.Kind.LAVA).count();
|
||||
acceptedWater+=water;acceptedLava+=lava;
|
||||
List<Map<String,Object>> regions=new ArrayList<>();
|
||||
var first=capacity.regionAt(-capacity.terrainLimit(),-capacity.terrainLimit());
|
||||
var last=capacity.regionAt(capacity.terrainLimit(),capacity.terrainLimit());
|
||||
for(int rx=first.x();rx<=last.x();rx++)for(int rz=first.z();rz<=last.z();rz++) {
|
||||
var region=new IslandCapacity.Region(rx,rz);
|
||||
double nx=Math.max(0,Math.max(region.minX(),-region.maxX())),nz=Math.max(0,Math.max(region.minZ(),-region.maxZ()));
|
||||
if(Math.hypot(nx,nz)>=capacity.terrainLimit())continue;
|
||||
var waterPlan=PopulationHydrologyRuntime.regionPlan(generator,state,region).water();
|
||||
var falls=Terrain25Runtime.falls(generator,state,region);
|
||||
for(var fall:falls) {
|
||||
helper.assertTrue(Math.hypot(fall.endX()-fall.startX(),fall.endZ()-fall.startZ())<=64.75,"Outlet search remains within64 blocks including integer rounding");
|
||||
helper.assertTrue(fall.width()>=8&&fall.width()<=16,"Waterfall lip width8–16");
|
||||
}
|
||||
totalFalls+=falls.size();
|
||||
regions.add(Map.of("region",List.of(rx,rz),"rivers",waterPlan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).count(),"falls",falls));
|
||||
}
|
||||
List<Map<String,Object>> chambers=new ArrayList<>();
|
||||
for(var c:plan.chambers()) {
|
||||
int dryFloor=0,open=0,roof=0;
|
||||
for(int dx=-12;dx<=12;dx+=4)for(int dz=-12;dz<=12;dz+=4){int x=(int)c.x()+dx,z=(int)c.z()+dz;
|
||||
if(view.sampleValue(context,x,c.floorY(),z)>0&&view.sampleValue(context,x,c.floorY()+1,z)<=0)dryFloor++;
|
||||
if(view.sampleValue(context,x,(int)c.y(),z)<=0)open++;
|
||||
if(view.sampleValue(context,x,(int)(c.y()+c.ry()+2),z)>0)roof++;
|
||||
}
|
||||
chambers.add(Map.of("kind",c.kind(),"center",List.of(c.x(),c.y(),c.z()),"radii",List.of(c.rx(),c.ry(),c.rz()),
|
||||
"floor_y",c.floorY(),"dry_floor_samples_of_49",dryFloor,"air_samples_of_49",open,"roof_samples_of_49",roof,"fluid_admitted",lakes.contains(c),"dry_floor_certified",plan.certifiedDryFloors().contains(c.id()),
|
||||
"dry_fit",plan.dryFits().getOrDefault(c.id(),new Terrain25Shape.DryFit(0,false,"not-a-dry-room"))));
|
||||
}
|
||||
row.put("certified_dry_floor_checks",verifyDryFloors(helper,view));
|
||||
row.put("native_calcite_coating",verifyCoating(helper,generator,state));
|
||||
row.put("exact_column_scan_equivalence",verifyColumnScans(helper,generator,state,capacity));
|
||||
row.put("maximum_surface",max);row.put("massif_fraction",raised/(double)columns);row.put("room_for_vegetation_columns",vegetationRoom);
|
||||
row.put("solid_surface_columns",solidColumns);row.put("sampled_columns",columns);row.put("chambers",chambers);
|
||||
row.put("admitted_water_lakes",water);row.put("admitted_lava_lakes",lava);row.put("regions",regions);
|
||||
row.put("density_digest",Long.toUnsignedString(digest,16));row.put("planning_ms",(System.nanoTime()-start)/1_000_000);
|
||||
reports.add(row); write(reportPath,Map.of("scope","Native samplers and plans only; no blocks, ticks, entities or visual verification", "complete",false,"worlds",reports));
|
||||
SanctuaryMod.LOGGER.info("Terrain25 diagnostics: seed {} size {} top {} water {} lava {} planning {}ms",seed,row.get("diameter"),max,water,lava,row.get("planning_ms"));
|
||||
helper.assertTrue(max<=358,"Terrain ceiling");
|
||||
helper.assertTrue(vegetationRoom>solidColumns*.5,"A majority of sampled existing land retains building and vegetation headroom");
|
||||
}
|
||||
boolean hasWater=acceptedWater>0,hasLava=acceptedLava>0;
|
||||
write(reportPath,Map.of("scope","Native samplers and plans only; no blocks, ticks, entities or visual verification","complete",true,
|
||||
"worlds",reports,"accepted_water_lakes",acceptedWater,"accepted_lava_lakes",acceptedLava,"accepted_waterfalls",totalFalls,
|
||||
"water_lake_witness",hasWater,"lava_lake_witness",hasLava));
|
||||
helper.assertTrue(hasWater,"At least one genuinely admitted water lake across the nine native witness worlds; refusal alone is not success");
|
||||
helper.assertTrue(hasLava,"At least one genuinely admitted lava lake across the nine native witness worlds; refusal alone is not success");
|
||||
helper.succeed();
|
||||
}
|
||||
private static Map<String,Object> verifyDryFloors(GameTestHelper helper,Terrain25Density.View view) {
|
||||
int tested=0,certified=0,rejected=0;var details=new ArrayList<Map<String,Object>>();
|
||||
for(var chamber:view.plan().chambers())if(chamber.kind()==Terrain25Shape.Kind.DRY) {
|
||||
var fit=view.plan().dryFits().get(chamber.id());
|
||||
helper.assertTrue(fit!=null&&fit.attempts()<=48,"Every dry-room natural fit has a bounded, explicit result");
|
||||
if(!fit.certified()){rejected++;details.add(Map.of("id",chamber.id(),"certified",false,"reason",fit.diagnostic()));continue;}
|
||||
certified++;int columns=0;
|
||||
for(int x=(int)Math.floor(chamber.x()-chamber.rx()-2);x<=chamber.x()+chamber.rx()+2;x++)
|
||||
for(int z=(int)Math.floor(chamber.z()-chamber.rz()-2);z<=chamber.z()+chamber.rz()+2;z++) {
|
||||
if(chamber.cut(x,chamber.floorY()+1,z)<=0)continue;columns++;
|
||||
// Full continuity is certified in the pure planner. Here a representative
|
||||
// native grid checks all three layers after every new cavity and fissure.
|
||||
if(Math.floorMod(x,3)!=0||Math.floorMod(z,3)!=0)continue;
|
||||
for(int y=chamber.floorY()-2;y<=chamber.floorY();y++) {
|
||||
helper.assertTrue(view.natural(SamplerContext.EMPTY_UNCACHED,x,y,z)>0,"A certified dry floor must already exist in intact rock");
|
||||
helper.assertTrue(view.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0,"Another cavity or fracture must not puncture a certified dry floor");tested++;
|
||||
}
|
||||
}
|
||||
details.add(Map.of("id",chamber.id(),"certified",true,"continuous_columns",columns,"attempts",fit.attempts()));
|
||||
}
|
||||
return Map.of("certified_rooms",certified,"refused_rooms",rejected,"native_floor_voxels",tested,"rooms",details);
|
||||
}
|
||||
/** Compare old/new loop results on actual native densities, and count their exact probes. */
|
||||
private static Map<String,Object> verifyColumnScans(GameTestHelper helper,NoiseBasedChunkGenerator generator,
|
||||
RandomState state,IslandCapacity capacity) {
|
||||
var view=Terrain25Runtime.view(generator,state);var coordinates=new LinkedHashSet<PopulationHydrology.Point>();
|
||||
for(int ix=-4;ix<=4;ix++)for(int iz=-4;iz<=4;iz++)
|
||||
coordinates.add(new PopulationHydrology.Point((int)Math.round(capacity.radius()*.95*ix/4),
|
||||
(int)Math.round(capacity.radius()*.95*iz/4)));
|
||||
for(var peak:view.plan().peaks())coordinates.add(new PopulationHydrology.Point((int)Math.round(peak.x()),(int)Math.round(peak.z())));
|
||||
int oldNatural=0,newNatural=0,oldGroves=0,newGroves=0,groveCases=0;
|
||||
for(var point:coordinates) {
|
||||
int x=point.x(),z=point.z();float[] intact=new float[384],finalDensity=new float[384];
|
||||
Arrays.fill(intact,-1);Arrays.fill(finalDensity,-1);
|
||||
for(int y=1;y<=358;y++) {
|
||||
intact[y]=view.natural(SamplerContext.EMPTY_UNCACHED,x,y,z);
|
||||
if(intact[y]>0)finalDensity[y]=view.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z);
|
||||
}
|
||||
int previousTop=-1,previousBottom=-1,optimizedTop=-1,optimizedBottom=-1;
|
||||
for(int y=358;y>0;y--){oldNatural++;if(intact[y]>0){previousTop=y;break;}}
|
||||
if(previousTop>=0)for(int y=1;y<=previousTop;y++){oldNatural++;if(intact[y]>0){previousBottom=y;break;}}
|
||||
int bound=Terrain25ColumnBounds.highestPossibleRock(view.plan().lift(x,z));
|
||||
for(int y=bound;y>=Terrain25ColumnBounds.LOWEST_POSSIBLE_ROCK;y--){newNatural++;if(intact[y]>0){optimizedTop=y;break;}}
|
||||
if(optimizedTop>=0)for(int y=Terrain25ColumnBounds.LOWEST_POSSIBLE_ROCK;y<=optimizedTop;y++){newNatural++;if(intact[y]>0){optimizedBottom=y;break;}}
|
||||
helper.assertTrue(previousTop==optimizedTop&&previousBottom==optimizedBottom,"Exact natural column bounds preserve both extrema at "+x+","+z);
|
||||
helper.assertTrue(Terrain25Runtime.naturalTop(generator,state,x,z)==previousTop,"Cached production natural top matches the original complete scan");
|
||||
for(int ceiling:new int[]{383,previousTop,previousTop-10}) {
|
||||
int before=-1,after=-1;
|
||||
for(int y=Math.min(383,ceiling);y>=24;y--){oldGroves++;if(finalDensity[y]>0){before=y;break;}}
|
||||
for(int y=Math.min(383,Math.min(ceiling,optimizedTop));y>=24;y--){newGroves++;if(finalDensity[y]>0){after=y;break;}}
|
||||
helper.assertTrue(before==after,"Groves retain the identical final natural surface with open, current or lowered ceiling");groveCases++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(newNatural<oldNatural&&newGroves<oldGroves,"The bounded scans must remove actual probes, not only preserve their results");
|
||||
var result=new LinkedHashMap<String,Object>();result.put("native_columns",coordinates.size());result.put("grove_ceiling_cases",groveCases);
|
||||
result.put("old_natural_density_probes",oldNatural);result.put("bounded_natural_density_probes",newNatural);
|
||||
result.put("old_grove_density_probes",oldGroves);result.put("bounded_grove_density_probes",newGroves);
|
||||
result.put("natural_probe_reduction",1-newNatural/(double)oldNatural);result.put("grove_probe_reduction",1-newGroves/(double)oldGroves);
|
||||
result.put("scope","Exact density-probe counts with unchanged native results; not a wall-clock speed claim");return result;
|
||||
}
|
||||
private static Map<String,Integer> verifyCoating(GameTestHelper helper,NoiseBasedChunkGenerator generator,RandomState state) {
|
||||
var view=Terrain25Runtime.view(generator,state);int core=0,shell=0;
|
||||
Terrain25Coating.Solid intact=(x,y,z)->view.natural(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;
|
||||
for(var peak:view.plan().peaks()) {
|
||||
int x=(int)Math.round(peak.x()),z=(int)Math.round(peak.z());
|
||||
for(int y=281;y<=358;y+=3) {
|
||||
if(!intact.at(x,y,z)||view.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)<=0)continue;
|
||||
boolean coats=Terrain25Coating.calcite(x,y,z,view.plan().lift(x,z),intact);
|
||||
var actual=Terrain25Runtime.finalBlock(generator,state,x,y,z,net.minecraft.world.level.block.Blocks.STONE.defaultBlockState());
|
||||
if(actual.isAir()||!actual.getFluidState().isEmpty())continue;
|
||||
helper.assertTrue(actual.is(net.minecraft.world.level.block.Blocks.CALCITE)==coats,"Cached calcite coating agrees with the intact native rock envelope");
|
||||
if(coats)shell++;else core++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(core>0,"High massifs retain an actual rock core beneath their calcite exterior");
|
||||
helper.assertTrue(shell>0,"High massifs retain an exposed calcite coating");
|
||||
return Map.of("checked_core_positions",core,"checked_coating_positions",shell);
|
||||
}
|
||||
private static int top(Terrain25Density.View view,SamplerContext context,int x,int z) {
|
||||
for(int y=358;y>0;y-=4)if(view.sampleValue(context,x,y,z)>0){for(int fine=Math.min(358,y+3);fine>=y;fine--)if(view.sampleValue(context,x,fine,z)>0)return fine;}
|
||||
return -1;
|
||||
}
|
||||
private static void write(Path path,Object report) throws Exception {Files.writeString(path,new GsonBuilder().setPrettyPrinting().create().toJson(report)+"\n");}
|
||||
}
|
||||
+539
@@ -0,0 +1,539 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.aerial.*;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** One real world per size/seed. Separate JVM phases preserve looting, mining and extracted occupants. */
|
||||
public final class Terrain25WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void naturalTerrainAndFiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try { begin(helper, report); }
|
||||
catch (Exception failure) { failure(report, failure); throw failure; }
|
||||
}
|
||||
|
||||
private static void begin(GameTestHelper helper, Map<String, Object> report) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 25, "Terrain25 requires a disposable generation25 world");
|
||||
String phase = System.getProperty("sanctuary.test.terrain25.phase", "create");
|
||||
helper.assertTrue(List.of("create", "reload1", "reload2").contains(phase), "Known aerial lifecycle phase");
|
||||
boolean create = phase.equals("create");
|
||||
var plan = generator.aerialPlan24();
|
||||
report.put("phase", phase); report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("experimental", generator.experimentalStructures());
|
||||
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
|
||||
boolean globalOff = !level.structureManager().shouldGenerateStructures();
|
||||
helper.assertTrue(create, "Options-OFF witness starts in a fresh disposable world");
|
||||
helper.assertTrue(plan.isEmpty() && plan.claimsChunks().isEmpty(), "Disabled aerial generation reserves no space");
|
||||
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
|
||||
var underground = fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(generator);
|
||||
helper.assertTrue(underground.starts().isEmpty() && underground.rooms().isEmpty(), "OptionsOFF leave no underground25 native start or dungeon room plan");
|
||||
helper.assertTrue(generator.mineralSanctuary23().isEmpty() && generator.ruinsPlan17().isEmpty()
|
||||
&& generator.heritagePlan19().sites().isEmpty() && generator.heritagePlan19().segments().isEmpty(),
|
||||
"OptionsOFF remove the optional mineral secret, workshop, observatory and abandoned railway plans");
|
||||
if (globalOff) helper.assertTrue(generator.transitPlan21().isEmpty() && generator.undergroundHall22().isEmpty(),
|
||||
"The global Minecraft switch also disables core traversal and hall");
|
||||
var binding = generator.starterContext();
|
||||
helper.assertTrue(fr.koka.sanctuary.worldgen.Terrain25Runtime.enabled(binding.generator()), "OptionsOFF retain the named generation25 terrain delegate");
|
||||
var shape = fr.koka.sanctuary.worldgen.Terrain25Runtime.shape(binding.generator(), binding.random());
|
||||
helper.assertTrue(!shape.peaks().isEmpty(), "OptionsOFF retain the natural mountain plan");
|
||||
var neutral = new LinkedHashSet<ChunkPos>();
|
||||
for (var peak : shape.peaks()) {
|
||||
int x = (int) Math.round(peak.x()), z = (int) Math.round(peak.z());
|
||||
int top = fr.koka.sanctuary.worldgen.Terrain25Runtime.naturalTop(binding.generator(), binding.random(), x, z);
|
||||
if (top > 260 && !generator.intersectsExperimentalStructures(x, top - 12, z, x, 383, z))
|
||||
neutral.add(ChunkPos.containing(new BlockPos(x, top, z)));
|
||||
if (neutral.size() == 3) break;
|
||||
}
|
||||
helper.assertTrue(neutral.size() == 3, "Three bounded high natural terrain witnesses are available with optionsOFF");
|
||||
var forbiddenOptional = Set.of("sanctuary:mineral_sanctuary", "sanctuary:forgotten_networks", "sanctuary:heritage_networks",
|
||||
"sanctuary:aerial_balloon", "sanctuary:aerial_merchant_ship", "sanctuary:aerial_mineral_islet",
|
||||
"sanctuary:underground_abandoned_village", "sanctuary:miner_cabin");
|
||||
var manufactured = Set.of(Blocks.RAIL, Blocks.POWERED_RAIL, Blocks.DETECTOR_RAIL, Blocks.ACTIVATOR_RAIL,
|
||||
Blocks.STONE_BRICKS, Blocks.STONE_BRICK_STAIRS, Blocks.CHISELED_STONE_BRICKS, Blocks.IRON_BARS, Blocks.SMOOTH_STONE);
|
||||
var terrainEvidence = new ArrayList<Map<String, Object>>();
|
||||
long vanillaStarts = 0, rails = 0;
|
||||
for (var at : neutral) {
|
||||
var chunk = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
var starts = chunk.getAllStarts().values().stream().filter(StructureStart::isValid).toList();
|
||||
if (globalOff) helper.assertTrue(starts.isEmpty(), "GlobalOFF emits no native start in real FULL terrain");
|
||||
for (var start : starts) {
|
||||
String key = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(start.getStructure()).toString();
|
||||
helper.assertTrue(!forbiddenOptional.contains(key), "ExperimentalOFF cannot emit an optional Sanctuary secret start: " + key);
|
||||
if (key.startsWith("minecraft:")) vanillaStarts++;
|
||||
}
|
||||
long highRock = 0, solids = 0;
|
||||
for (var pos : BlockPos.betweenClosed(at.getMinBlockX(), 1, at.getMinBlockZ(), at.getMaxBlockX(), 383, at.getMaxBlockZ())) {
|
||||
var state = chunk.getBlockState(pos);
|
||||
if (state.is(net.minecraft.tags.BlockTags.RAILS)) rails++;
|
||||
if (globalOff) helper.assertTrue(!manufactured.contains(state.getBlock()), "GlobalOFF leaves no secret architecture or rail blocks");
|
||||
if (state.isCollisionShapeFullBlock(level, pos) && !state.is(net.minecraft.tags.BlockTags.LEAVES)
|
||||
&& !state.is(net.minecraft.tags.BlockTags.LOGS)) {
|
||||
solids++; if (pos.getY() >= 250 && pos.getY() <= 358) highRock++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(highRock > 0 && solids > 0, "OptionsOFF retain real high generation25 land, not only an empty plan");
|
||||
var biome = level.getBiome(new BlockPos(at.getMiddleBlockX(), 270, at.getMiddleBlockZ()));
|
||||
helper.assertTrue(biome.unwrapKey().orElseThrow().identifier().toString().startsWith("sanctuary:terrain25_"),
|
||||
"OptionsOFF retain the generation25 natural biome distribution");
|
||||
terrainEvidence.add(Map.of("chunk", List.of(at.x(), at.z()), "solid_blocks", solids, "rock_above_y250", highRock,
|
||||
"biome", biome.unwrapKey().orElseThrow().identifier().toString(), "native_starts", starts.size()));
|
||||
}
|
||||
report.put("off_mode", globalOff ? "global" : "experimental_only"); report.put("native_terrain", terrainEvidence);
|
||||
report.put("underground25_plan_empty", true); report.put("optional_secret_and_rail_plans_empty", true);
|
||||
report.put("vanilla_generation_allowed", !globalOff); report.put("vanilla_starts_in_sample", vanillaStarts);
|
||||
report.put("rail_blocks_in_sample", rails); report.put("core_traversal_modules", generator.transitPlan21().modules().size());
|
||||
report.put("core_hall_networks", generator.undergroundHall22().networks().size());
|
||||
report.put("scope", globalOff ? "No structure starts or constructed blocks in three FULL native terrain witnesses."
|
||||
: "Optional Sanctuary plans and starts disabled; core traversal, core hall and vanilla structures retain their own rules. Native mine rails are allowed.");
|
||||
report.put("passed", true); write(Path.of("diagnostics/terrain25/world/" + (globalOff ? "global-off" : "experimental-off") + ".json"), report);
|
||||
helper.succeed(); return;
|
||||
}
|
||||
helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
|
||||
if (generator.initialDiameter() == 724 && level.getSeed() == 0)
|
||||
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/terrain25/world/blocks.json"), plan.sites());
|
||||
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
|
||||
report.put("census_before", StructureCensusService.aerialSites(level));
|
||||
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
|
||||
var journalBefore = files(level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()));
|
||||
verifyClaims(helper, generator, plan, report);
|
||||
Path world = level.getServer().getWorldPath(LevelResource.ROOT);
|
||||
JsonObject previous = create ? null : JsonParser.parseString(Files.readString(world.resolve("terrain25-" + (phase.equals("reload1") ? "create" : "reload1") + ".json"))).getAsJsonObject();
|
||||
if (previous != null) {
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload occurs in a separate JVM after a successful phase");
|
||||
helper.assertTrue(previous.get("geometry").equals(JSON.toJsonTree(plan.sites())), "Site geometry and palette seeds survive reload");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(report.get("generator_codec")), "Generator options survive reload");
|
||||
}
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
plan.sites().forEach(site -> chunks.addAll(site.claimedChunks()));
|
||||
plan.sites().forEach(site -> chunks.add(ChunkPos.containing(new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ()))));
|
||||
// The two extracted merchants are retained in a separate, existing root chunk.
|
||||
chunks.add(new ChunkPos(0, 0));
|
||||
var starter = generator.starterContext();
|
||||
var terrainPlan = fr.koka.sanctuary.worldgen.Terrain25Runtime.shape(starter.generator(), starter.random());
|
||||
var terrainChunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
for (var chamber : terrainPlan.chambers()) {
|
||||
int cx = (int)Math.floor(chamber.x()) >> 4, cz = (int)Math.floor(chamber.z()) >> 4;
|
||||
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)terrainChunks.add(new ChunkPos(cx+dx,cz+dz));
|
||||
}
|
||||
chunks.addAll(terrainChunks);
|
||||
var loadOrder = new ArrayList<>(chunks);
|
||||
boolean reverseChunks = Boolean.getBoolean("sanctuary.test.terrain25.reverseChunks");
|
||||
if (reverseChunks) Collections.reverse(loadOrder);
|
||||
report.put("reverse_chunks", reverseChunks);
|
||||
var iterator = loadOrder.iterator();
|
||||
long fullStarted=System.nanoTime();long[] fullNanos={0};int[] fullCalls={0};
|
||||
report.put("heap_before_full",Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
|
||||
var entityChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().stream().filter(AerialPlan24.Site::merchant).forEach(site -> entityChunks.addAll(site.claimedChunks()));
|
||||
entityChunks.add(new ChunkPos(0, 0));
|
||||
// Only refuge chunks contain initial inhabitants; other cave blocks need FULL, not entity simulation.
|
||||
for (var chamber : terrainPlan.chambers()) if (chamber.kind() == fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.MUSHROOM) {
|
||||
int cx = (int)Math.floor(chamber.x()) >> 4, cz = (int)Math.floor(chamber.z()) >> 4;
|
||||
for (int dx=-1; dx<=1; dx++) for (int dz=-1; dz<=1; dz++) entityChunks.add(new ChunkPos(cx+dx,cz+dz));
|
||||
}
|
||||
boolean[] tickets = {false}; int[] wait = {0};
|
||||
Runnable release = () -> {
|
||||
if (tickets[0]) entityChunks.forEach(pos -> ChunkTickets25.remove(level, pos));
|
||||
tickets[0] = false;
|
||||
};
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var chunk = iterator.next(); report.put("stage", "FULL " + chunk);
|
||||
long chunkStart=System.nanoTime();
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||
fullNanos[0]+=System.nanoTime()-chunkStart;fullCalls[0]++;
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!tickets[0]) {
|
||||
entityChunks.forEach(pos -> ChunkTickets25.add(level, pos)); tickets[0] = true;
|
||||
}
|
||||
var occupants = occupants(level, plan);
|
||||
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
|
||||
for (var pos : entityChunks) {
|
||||
boolean loaded = level.areEntitiesLoaded(pos.pack()), ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
|
||||
readiness.put(pos.toString(), Map.of("loaded", loaded, "ticking", ticking, "native_debug", level.getChunkSource().getChunkDebugData(pos)));
|
||||
entitiesReady &= loaded && ticking;
|
||||
}
|
||||
report.put("entity_chunks", readiness);
|
||||
report.put("occupants_observed", occupants.stream().map(villager -> Map.of("uuid", villager.getUUID().toString(),
|
||||
"position", List.of(villager.getX(), villager.getY(), villager.getZ()))).toList());
|
||||
report.put("visible_occupants", occupants.size()); report.put("entity_wait_ticks", wait[0]);
|
||||
if ((!entitiesReady || occupants.size() < 2) && wait[0]++ < 6000) {
|
||||
report.put("stage", "wait for persistent merchants");
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
helper.assertTrue(entitiesReady, "All ship chunks finish native entity loading and become ticking before occupant inspection");
|
||||
helper.assertTrue(occupants.size() == 2, "Both one-time merchants are present after native entity loading");
|
||||
report.put("full_get_chunk_calls",fullCalls[0]);report.put("full_get_chunk_ms",fullNanos[0]/1_000_000);
|
||||
report.put("full_loop_elapsed_ms",(System.nanoTime()-fullStarted)/1_000_000);
|
||||
report.put("heap_after_full",Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
|
||||
report.put("memory_pool_peak",java.lang.management.ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.map(pool->Map.of("name",pool.getName(),"used",pool.getPeakUsage().getUsed(),"committed",pool.getPeakUsage().getCommitted())).toList());
|
||||
report.put("observed_server_ticks",helper.getTick());
|
||||
report.put("stage", "inspect native structures");
|
||||
inspectTerrain(helper, generator, terrainChunks, report, create, previous);
|
||||
var starts = starts(helper, level, plan);
|
||||
var before = inspect(helper, level, plan, create, report);
|
||||
if (create) {
|
||||
verifyLocator(helper, level, plan, starts);
|
||||
report.put("stage", "loot, extract and dismantle disposable witnesses");
|
||||
report.put("mutations", mutate(helper, level, plan, occupants, before));
|
||||
} else {
|
||||
report.put("mutations", previous.get("mutations"));
|
||||
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
|
||||
}
|
||||
var tags = tags(level, starts);
|
||||
if (!create) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "Native starts, references and occupant flags survive both reloads");
|
||||
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
|
||||
report.put("blocks_after", countSites(level, plan));
|
||||
var census = StructureCensusService.aerialSites(level);
|
||||
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
"Five historical generated sites remain indexed after loot, mining and demolition");
|
||||
report.put("census_after", census);
|
||||
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
|
||||
report.put("expansion_journal_unchanged", true);
|
||||
Mineral25NativeChecks.verify(helper, report);
|
||||
StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
|
||||
report.put("fluid_scheduler", FluidTiming25.snapshot());
|
||||
report.put("passed", true);
|
||||
write(world.resolve("terrain25-" + phase + ".json"), report);
|
||||
write(Path.of("diagnostics/terrain25/world/" + phase + ".json"), report);
|
||||
release.run(); helper.succeed();
|
||||
} catch (Exception error) {
|
||||
release.run(); failure(report, error); throw new IllegalStateException("Terrain25 failed: " + error.getMessage(), error);
|
||||
}
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
}
|
||||
|
||||
private static void inspectTerrain(GameTestHelper helper, SanctuaryChunkGenerator generator, Set<ChunkPos> terrainChunks,
|
||||
Map<String,Object> report, boolean create, JsonObject previous) {
|
||||
var level=helper.getLevel();var binding=generator.starterContext();
|
||||
var plan=fr.koka.sanctuary.worldgen.Terrain25Runtime.shape(binding.generator(),binding.random());
|
||||
var counts=new TreeMap<String,Integer>();
|
||||
for(var cp:terrainChunks)for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)
|
||||
for(int y=1;y<364;y++) {
|
||||
var state=level.getBlockState(new BlockPos(x,y,z));
|
||||
if(!state.isAir())counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(),1,Integer::sum);
|
||||
}
|
||||
report.put("terrain_blocks",counts);report.put("terrain_sample_chunks",terrainChunks.size());
|
||||
var chambers=new ArrayList<Map<String,Object>>();
|
||||
for(var c:plan.chambers()) {
|
||||
int walkable=0,water=0,lava=0;
|
||||
for(int dx=-12;dx<=12;dx+=3)for(int dz=-12;dz<=12;dz+=3) {
|
||||
var floor=new BlockPos((int)c.x()+dx,c.floorY(),(int)c.z()+dz);
|
||||
if(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir()&&level.getBlockState(floor.above(2)).isAir())walkable++;
|
||||
if(level.getBlockState(floor.above()).is(Blocks.WATER))water++;
|
||||
if(level.getBlockState(floor.above()).is(Blocks.LAVA))lava++;
|
||||
}
|
||||
chambers.add(Map.of("id",c.id(),"kind",c.kind(),"walkable_samples",walkable,"water_samples",water,"lava_samples",lava));
|
||||
}
|
||||
report.put("actual_chambers",chambers);
|
||||
var cows=level.getEntitiesOfClass(net.minecraft.world.entity.animal.cow.MushroomCow.class,new AABB(-2048,0,-2048,2048,384,2048));
|
||||
var ids=cows.stream().map(c->c.getUUID().toString()).sorted().toList();
|
||||
report.put("mooshrooms",ids);
|
||||
if(create) {
|
||||
var changes=new TreeMap<String,Object>();
|
||||
if(!cows.isEmpty()) {
|
||||
var moved=cows.getFirst();
|
||||
var at=new BlockPos(12,301,12);
|
||||
for(int x=10;x<=14;x++)for(int z=10;z<=14;z++)level.setBlock(new BlockPos(x,300,z),Blocks.GRASS_BLOCK.defaultBlockState(),3);
|
||||
moved.setNoAi(true); moved.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);
|
||||
changes.put("extracted_uuid",moved.getUUID().toString());
|
||||
if(cows.size()>1){var removed=cows.get(1);changes.put("removed_uuid",removed.getUUID().toString());removed.discard();}
|
||||
}
|
||||
report.put("mooshroom_mutations",changes);
|
||||
report.put("mooshroom_survivors",cows.stream().filter(c->!c.isRemoved()).map(c->c.getUUID().toString()).sorted().toList());
|
||||
} else {
|
||||
helper.assertTrue(previous.get("mooshroom_survivors").equals(JSON.toJsonTree(ids)),"Removed and extracted mooshrooms are not regenerated on reload");
|
||||
report.put("mooshroom_mutations",previous.get("mooshroom_mutations"));report.put("mooshroom_survivors",previous.get("mooshroom_survivors"));
|
||||
var changes=previous.getAsJsonObject("mooshroom_mutations");
|
||||
if(changes.has("extracted_uuid")) {
|
||||
var uuid=UUID.fromString(changes.get("extracted_uuid").getAsString());
|
||||
helper.assertTrue(cows.stream().anyMatch(c->c.getUUID().equals(uuid)&&c.getY()>295),"Extracted mooshroom remains outside its original cave");
|
||||
}
|
||||
}
|
||||
if(Boolean.getBoolean("sanctuary.test.terrain25.requireMooshrooms"))helper.assertTrue(!cows.isEmpty(),"Selected native witness contains actual mooshrooms, not merely a proposed refuge");
|
||||
if(Boolean.getBoolean("sanctuary.test.terrain25.requireWaterLake"))helper.assertTrue(chambers.stream().anyMatch(c->c.get("kind")==fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.LAKE && (int)c.get("water_samples")>0),"Selected native witness contains real underground lake water");
|
||||
if(Boolean.getBoolean("sanctuary.test.terrain25.requireLavaLake"))helper.assertTrue(chambers.stream().anyMatch(c->c.get("kind")==fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.LAVA && (int)c.get("lava_samples")>0),"Selected native witness contains real underground lake lava");
|
||||
}
|
||||
|
||||
private static void verifyClaims(GameTestHelper helper, SanctuaryChunkGenerator generator, AerialPlan24 plan, Map<String, Object> report) {
|
||||
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var views = new LinkedHashMap<String, Object>();
|
||||
for (var site : plan.sites()) {
|
||||
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
|
||||
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 25);
|
||||
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
|
||||
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
|
||||
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
|
||||
}
|
||||
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 25)),
|
||||
"Unclaimed space remains available; there is no blanket exclusion ring");
|
||||
report.put("look_views", views);
|
||||
}
|
||||
|
||||
private static List<Villager> occupants(ServerLevel level, AerialPlan24 plan) {
|
||||
var expected = plan.sites().stream().filter(AerialPlan24.Site::merchant).map(AerialGeometry24::occupantUuid).collect(java.util.stream.Collectors.toSet());
|
||||
return level.getEntitiesOfClass(Villager.class, new AABB(-2048, 0, -2048, 2048, 384, 2048))
|
||||
.stream().filter(villager -> expected.contains(villager.getUUID())).toList();
|
||||
}
|
||||
|
||||
private static Map<String, StructureStart> starts(GameTestHelper helper, ServerLevel level, AerialPlan24 plan) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var result = new TreeMap<String, StructureStart>();
|
||||
for (var site : plan.sites()) {
|
||||
var type = registry.get(Identifier.parse(AerialStructures24.structureId(site))).orElseThrow().value();
|
||||
var start = level.getChunk(site.startChunkX(), site.startChunkZ(), ChunkStatus.FULL, true).getStartForStructure(type);
|
||||
helper.assertTrue(start != null && start.isValid(), "Planned aerial site has a real persisted native start: " + site.id());
|
||||
helper.assertTrue(start.getPieces().stream().filter(piece -> piece instanceof AerialPiece24 aerial && aerial.site().equals(site)).count() == 1,
|
||||
"Exactly one piece retains the site's immutable descriptor");
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var tag = start.createTag(context, start.getChunkPos());
|
||||
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, start.getChunkPos())), "Aerial start round-trips through native serialization");
|
||||
if (site.merchant()) helper.assertTrue(start.getPieces().stream().filter(AerialPiece24.class::isInstance).map(AerialPiece24.class::cast)
|
||||
.allMatch(AerialPiece24::occupantPlaced), "Merchant placement flag is stored after its first creation");
|
||||
result.put(site.id(), start);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Map<BlockPos, BlockState>> inspect(GameTestHelper helper, ServerLevel level, AerialPlan24 plan,
|
||||
boolean fresh, Map<String, Object> report) {
|
||||
var expected = new LinkedHashMap<String, Map<BlockPos, BlockState>>(); int chestCount = 0, waterSources = 0;
|
||||
var vegetation = new TreeMap<String, Object>();
|
||||
var shores = new TreeMap<String, Object>(); report.put("shore_witnesses", shores);
|
||||
for (var site : plan.sites()) {
|
||||
var model = render(site); expected.put(site.id(), model);
|
||||
var shore = new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ());
|
||||
var column = new TreeMap<Integer, String>();
|
||||
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
|
||||
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
var chestAt = AerialGeometry24.chest(site);
|
||||
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
|
||||
if (fresh) {
|
||||
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
|
||||
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
|
||||
for (var pos : AerialGeometry24.interiorChecks(site)) {
|
||||
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
|
||||
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
|
||||
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
|
||||
}
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
|
||||
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
|
||||
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
|
||||
vegetation.put(site.id(), vegetation(helper, level, site));
|
||||
if (site.waterfall()) {
|
||||
var source = AerialGeometry24.waterfallSource(site);
|
||||
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
|
||||
}
|
||||
// These columns are beyond the authored box and well outside initial terrain ownership.
|
||||
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
|
||||
var state = level.getBlockState(pos.atY(y));
|
||||
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
|
||||
}
|
||||
}
|
||||
helper.assertTrue(chestCount == 5 && waterSources == 1, "Five physical chests and exactly one planned source");
|
||||
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
|
||||
report.put("vegetation", vegetation);
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
|
||||
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
"Tree headroom preserves the original stone height");
|
||||
int surface = 0, grass = 0;
|
||||
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
|
||||
int top = AerialGeometry24.terrainTop(site, x, z);
|
||||
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
|
||||
surface++;
|
||||
if (level.getBlockState(AerialGeometry24.world(site, x, top, z)).is(Blocks.GRASS_BLOCK)) grass++;
|
||||
}
|
||||
helper.assertTrue(grass * 2 > surface, "Most real intact surface columns carry grass: " + site.id());
|
||||
var trees = AerialGeometry24.trees(site);
|
||||
helper.assertTrue(!trees.isEmpty(), "At least one real tree on each mineral islet");
|
||||
for (var tree : trees) {
|
||||
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
|
||||
var floor = level.getBlockState(ground);
|
||||
helper.assertTrue(floor.is(Blocks.GRASS_BLOCK) || floor.is(Blocks.DIRT), "Tree stands on soil, never in the air");
|
||||
for (int y = 1; y <= tree.trunkHeight(); y++)
|
||||
helper.assertTrue(level.getBlockState(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Native tree trunk stays continuous");
|
||||
var crown = ground.above(tree.trunkHeight() + 1);
|
||||
var leaves = level.getBlockState(crown);
|
||||
helper.assertTrue(site.bounds().isInside(crown) && leaves.is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES)
|
||||
&& !leaves.getValue(BlockStateProperties.PERSISTENT) && leaves.getValue(BlockStateProperties.DISTANCE) < 7,
|
||||
"Complete natural crown survives native chunk decoration and reload");
|
||||
}
|
||||
return Map.of("rock_height", site.rockHeight(), "headroom", AerialPlan24.VEGETATION_HEADROOM,
|
||||
"surface_columns", surface, "grass_columns", grass, "grass_percent", 100.0 * grass / surface, "trees", trees);
|
||||
}
|
||||
|
||||
private static Map<String, Object> mutate(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, List<Villager> occupants,
|
||||
Map<String, Map<BlockPos, BlockState>> models) {
|
||||
var emptied = new ArrayList<List<Integer>>(); var removed = new ArrayList<List<Integer>>(); var loot = new TreeMap<String, Integer>();
|
||||
for (var site : plan.sites()) {
|
||||
var at = AerialGeometry24.chest(site); var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
|
||||
chest.unpackLootTable(null); int quantity = 0;
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
|
||||
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
|
||||
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
|
||||
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
|
||||
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
|
||||
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
|
||||
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
|
||||
}
|
||||
}
|
||||
var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow();
|
||||
var demolition = AerialGeometry24.entry(balloon).below();
|
||||
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
|
||||
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
|
||||
var extracted = new TreeMap<String, List<Double>>(); int i = 0;
|
||||
for (var villager : occupants.stream().sorted(Comparator.comparing(v -> v.getUUID().toString())).toList()) {
|
||||
// This fixture parks extracted merchants in an existing root chunk, outside both ships.
|
||||
villager.setNoAi(true); villager.setNoGravity(true); villager.setDeltaMovement(Vec3.ZERO);
|
||||
villager.snapTo(4.5 + i++ * 4, 330, 4.5); villager.setPersistenceRequired();
|
||||
extracted.put(villager.getUUID().toString(), List.of(villager.getX(), villager.getY(), villager.getZ()));
|
||||
}
|
||||
return Map.of("emptied_chests", emptied, "removed_blocks", removed, "extracted_merchants", extracted, "loot_items_obtained", loot);
|
||||
}
|
||||
|
||||
private static void verifyMutations(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, JsonObject mutations, List<Villager> occupants) {
|
||||
for (var value : mutations.getAsJsonArray("removed_blocks")) {
|
||||
var pos = position(value.getAsJsonArray()); helper.assertTrue(level.getBlockState(pos).isAir(), "Mined ore and dismantled floor remain absent after reload");
|
||||
}
|
||||
var extracted = mutations.getAsJsonObject("extracted_merchants");
|
||||
for (var villager : occupants) {
|
||||
var at = extracted.getAsJsonArray(villager.getUUID().toString());
|
||||
helper.assertTrue(at != null && Math.abs(villager.getX() - at.get(0).getAsDouble()) < .01
|
||||
&& Math.abs(villager.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(villager.getZ() - at.get(2).getAsDouble()) < .01,
|
||||
"Extracted merchant stays outside its original ship without respawning");
|
||||
}
|
||||
for (var site : plan.sites()) if (site.merchant()) helper.assertTrue(level.getEntitiesOfClass(Villager.class, AABB.of(site.bounds())).isEmpty(),
|
||||
"The visited ship does not replenish an extracted merchant");
|
||||
}
|
||||
|
||||
private static void verifyLocator(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, Map<String, StructureStart> starts) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var holders = new ArrayList<Holder<Structure>>();
|
||||
plan.sites().stream().map(AerialStructures24::structureId).distinct().forEach(id -> holders.add(registry.get(Identifier.parse(id)).orElseThrow()));
|
||||
holders.add(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow());
|
||||
var wanted = HolderSet.direct(holders); var origin = plan.sites().getFirst().locator();
|
||||
var activeBefore = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands();
|
||||
int before = starts.values().stream().mapToInt(StructureStart::getReferences).sum();
|
||||
var found = ExpansionStructures23.findNearest(level, wanted, origin, 100, false);
|
||||
helper.assertTrue(found != null, "Mixed vanilla/aerial locator finds a real accepted start");
|
||||
helper.assertTrue(starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before, "Ordinary locate never marks a site referenced");
|
||||
var referenced = ExpansionStructures23.findNearest(level, wanted, origin, 100, true);
|
||||
helper.assertTrue(referenced != null && starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before + 1,
|
||||
"Mixed map lookup references exactly its one winning aerial start");
|
||||
helper.assertTrue(activeBefore.equals(((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands()), "Locator never activates an expansion");
|
||||
}
|
||||
|
||||
private static Map<BlockPos, BlockState> render(AerialPlan24.Site site) {
|
||||
var result = new HashMap<BlockPos, BlockState>();
|
||||
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
|
||||
public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
|
||||
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
private static Map<String, Map<String, Integer>> countSites(ServerLevel level, AerialPlan24 plan) {
|
||||
var all = new TreeMap<String, Map<String, Integer>>();
|
||||
for (var site : plan.sites()) {
|
||||
var counts = new TreeMap<String, Integer>();
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) for (int y = site.minY(); y <= site.maxY(); y++) {
|
||||
var state = level.getBlockState(new BlockPos(x, y, z));
|
||||
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
}
|
||||
all.put(site.id(), counts);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
private static boolean ore(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); }
|
||||
private static Map<String, String> tags(ServerLevel level, Map<String, StructureStart> starts) {
|
||||
var result = new TreeMap<String, String>(); var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
starts.forEach((id, start) -> result.put(id, start.createTag(context, start.getChunkPos()).toString())); return result;
|
||||
}
|
||||
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos position(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
|
||||
var found = new TreeMap<String, byte[]>();
|
||||
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
|
||||
for (var path : paths.filter(Files::isRegularFile).toList()) found.put(directory.relativize(path).toString(), Files.readAllBytes(path));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
private static boolean equalFiles(Map<String, byte[]> a, Map<String, byte[]> b) {
|
||||
return a.keySet().equals(b.keySet()) && a.entrySet().stream().allMatch(entry -> Arrays.equals(entry.getValue(), b.get(entry.getKey())));
|
||||
}
|
||||
private static void write(Path path, Map<String, Object> report) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(report)); }
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("failure", error.toString());
|
||||
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
|
||||
SanctuaryMod.LOGGER.error("Terrain25 fixture failed at {}", report.get("stage"), error);
|
||||
try { write(Path.of("diagnostics/terrain25/world/failure.json"), report); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
|
||||
}
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Shape;
|
||||
import fr.koka.sanctuary.worldgen.aerial.AerialPlanner24;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlanner25;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
|
||||
/**
|
||||
* Native-registry seed search. No evaluated chunks, starts, manifests or reservations are published.
|
||||
* Properties under sanctuary.test.underground25.search: seeds, diameter, and optional required families.
|
||||
* Required families stop the search once all have witnesses; an exhausted search may legitimately find none.
|
||||
* dryOnly skips every structure planner, accepts at most32 seeds, and optionally stopsAfterFirstDry.
|
||||
*/
|
||||
public final class Underground25SeedSearch {
|
||||
private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final String PREFIX = "sanctuary.test.underground25.search.";
|
||||
private static final Path OUTPUT = Path.of("diagnostics/underground25/search.json");
|
||||
private static final int MAX_SEEDS = 24;
|
||||
private static final int MAX_DRY_SEEDS = 32;
|
||||
private static final List<String> FAMILIES = List.of("sanctuary:underground_abandoned_village",
|
||||
"sanctuary:miner_cabin", "minecraft:mineshaft", "minecraft:swamp_hut");
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void conditionalNativePlansAcrossSeeds(GameTestHelper helper) throws Exception {
|
||||
if (Boolean.getBoolean(PREFIX + "dryOnly")) { dryOnly(helper); return; }
|
||||
ServerLevel level = helper.getLevel();
|
||||
helper.assertTrue(level.structureManager().shouldGenerateStructures(), "Seed search requires general structures ON");
|
||||
helper.assertTrue(level.getMinY() == 0 && level.getHeight() == 384, "The host must supply the real Sanctuary vertical extent");
|
||||
var seeds = seeds(System.getProperty(PREFIX + "seeds", "0,42,2026"));
|
||||
var required = families(System.getProperty(PREFIX + "required", ""));
|
||||
int diameter = Integer.getInteger(PREFIX + "diameter", Integer.getInteger("sanctuary.test.diameter", 724));
|
||||
helper.assertTrue(diameter == 512 || diameter == 724 || diameter == 1024, "Use a supported initial diameter");
|
||||
var preset = level.registryAccess().lookupOrThrow(Registries.WORLD_PRESET)
|
||||
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary"))).value();
|
||||
var prototype = (SanctuaryChunkGenerator) preset.createWorldDimensions().overworld();
|
||||
helper.assertTrue(prototype.rootGeneration() == 25, "Registry preset must really be generation25");
|
||||
Object liveGenerator = level.getChunkSource().getGenerator();
|
||||
var live = liveGenerator instanceof SanctuaryChunkGenerator g ? g : null;
|
||||
var liveIslands = live == null ? List.<ExpansionIsland>of() : List.copyOf(live.source().islands());
|
||||
Object liveUnderground = live == null ? null : UndergroundStructures25.plan(live);
|
||||
Object liveAerial = live == null ? null : live.aerialPlan24();
|
||||
Object liveMineral = live == null ? null : live.mineralSanctuary23();
|
||||
Object liveTransit = live == null ? null : live.transitPlan21();
|
||||
Object liveHall = live == null ? null : live.undergroundHall22();
|
||||
Object liveHeritage = live == null ? null : live.heritagePlan19();
|
||||
Object liveRuins = live == null ? null : live.ruinsPlan17();
|
||||
|
||||
var rows = new ArrayList<Map<String, Object>>();
|
||||
var witnesses = new LinkedHashMap<String, List<Long>>();
|
||||
FAMILIES.forEach(id -> witnesses.put(id, new ArrayList<>()));
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("scope", "Native registries, real generation25 terrain and conditional admission plans only; no FULL chunks, blocks, entities, loot or visual validation");
|
||||
report.put("selection", "Unmodified production admission, graph sizes, priority reservations and per-stage sample budgets; absence is a valid search result");
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("host_seed", level.getSeed()); report.put("diameter", diameter);
|
||||
report.put("requested_seeds", seeds); report.put("maximum_seeds", MAX_SEEDS);
|
||||
report.put("required_plan_families", required);
|
||||
report.put("complete", false); report.put("worlds", rows); report.put("positive_plan_witnesses", witnesses);
|
||||
report.put("evaluated_world_chunks_requested", 0);
|
||||
write(OUTPUT, report);
|
||||
int[] index = {0}; boolean[] failures = {false};
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
boolean foundRequired = !required.isEmpty() && required.stream().allMatch(id -> !witnesses.get(id).isEmpty());
|
||||
if (index[0] == seeds.size() || foundRequired) {
|
||||
report.remove("current_seed"); report.put("complete", true);
|
||||
report.put("termination", foundRequired ? "required_families_found" : "requested_seeds_exhausted");
|
||||
report.put("required_families_found", foundRequired);
|
||||
report.put("evaluated_seeds", seeds.subList(0, index[0]));
|
||||
report.put("unevaluated_seeds", seeds.subList(index[0], seeds.size()));
|
||||
report.put("all_evaluations_succeeded", !failures[0]); write(OUTPUT, report);
|
||||
helper.assertTrue(!failures[0], "Every requested seed must finish its plan; inspect incremental search.json for failures");
|
||||
helper.succeed(); return;
|
||||
}
|
||||
long seed = seeds.get(index[0]++), started = System.nanoTime();
|
||||
var row = new LinkedHashMap<String, Object>(); row.put("seed", seed); row.put("diameter", diameter);
|
||||
rows.add(row); report.put("current_seed", seed); write(OUTPUT, report);
|
||||
SanctuaryMod.LOGGER.info("Underground25 seed search {}/{}: seed {} diameter {}", index[0], seeds.size(), seed, diameter);
|
||||
try {
|
||||
// Copies come from the registered preset, not the already activated host biome source.
|
||||
var generator = prototype.withInitialDiameter(diameter).withExperimentalStructures(true);
|
||||
helper.assertTrue(generator != liveGenerator && generator.source() != prototype.source(), "Each seed has its own generator and activation snapshot");
|
||||
row.put("stage", "core_secrets"); write(OUTPUT, report);
|
||||
generator.prepareAerialSampling24(level, seed);
|
||||
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
helper.assertTrue(root.seed() == seed && generator.randomState(root).seed() == seed, "Both descriptor and native random state use the requested seed");
|
||||
row.put("core_preparation_ms", millis(started));
|
||||
long aerialStarted = System.nanoTime(); row.put("stage", "aerial_reservations"); write(OUTPUT, report);
|
||||
generator.setAerialPlan24(AerialPlanner24.plan(seed, diameter, new AerialPlanner24.Terrain() {
|
||||
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
|
||||
@Override public boolean reserved(int x, int z, int minY, int maxY) {
|
||||
var binding = generator.starterContext();
|
||||
return Terrain25Runtime.protects(binding.generator(), binding.random(), new BlockPos(x, minY, z), 0, maxY - minY, 0)
|
||||
|| generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
|
||||
}
|
||||
}));
|
||||
row.put("aerial_preparation_ms", millis(aerialStarted));
|
||||
row.put("aerial_sites", generator.aerialPlan24().sites().size());
|
||||
helper.assertTrue(generator.aerialPlan24().sites().size() == 5, "Keep all five earlier satellite reservations");
|
||||
row.put("stage", "conditional_admission"); write(OUTPUT, report);
|
||||
long conditionalStarted = System.nanoTime();
|
||||
var plan = UndergroundPlanner25.plan(level, generator);
|
||||
row.put("conditional_planning_ms", millis(conditionalStarted));
|
||||
row.put("columns", plan.columns()); row.put("diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.columns() <= UndergroundPlanner25.MAX_COLUMNS * 4, "Preserve the independent finite sampling budgets");
|
||||
helper.assertTrue(UndergroundStructures25.plan(generator).starts().isEmpty(), "Diagnostic plans are never published as active starts");
|
||||
row.put("planned_starts", plan.starts().stream().map(p -> Map.of("id", p.key().identifier().toString(),
|
||||
"chunk", List.of(p.start().getChunkPos().x(), p.start().getChunkPos().z()), "entry", position(p.entrance()), "pieces", p.start().getPieces().size())).toList());
|
||||
row.put("planned_start_bounds", plan.starts().stream().map(p -> Map.of("id", p.key().identifier().toString(),
|
||||
"chunk", List.of(p.start().getChunkPos().x(), p.start().getChunkPos().z()), "bounds", bounds(p.start().getBoundingBox()))).toList());
|
||||
row.put("planned_rooms", plan.rooms().stream().map(p -> Map.of("entry", position(p.getLocatorPosition()), "seed", p.seed())).toList());
|
||||
var shape = Terrain25Runtime.shape(generator.delegate(root), generator.randomState(root));
|
||||
row.put("dry_chambers", shape.chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.DRY).map(c -> Map.of(
|
||||
"id", c.id(), "center", List.of(c.x(), c.y(), c.z()), "radii", List.of(c.rx(), c.ry(), c.rz()),
|
||||
"rock_floor_y", c.floorY(), "dry_fit", shape.dryFits().get(c.id()),
|
||||
"central_candidate_chunks", UndergroundPlanner25.centralVillageChunks(c).stream().map(p -> List.of(p.x(), p.z())).toList())).toList());
|
||||
var wetlands = new ArrayList<Map<String, Object>>();
|
||||
for (var region : PopulationHydrologyRuntime.plansIntersecting(generator.delegate(root), generator.randomState(root), -root.terrainRadius(),
|
||||
-root.terrainRadius(), root.terrainRadius(), root.terrainRadius())) {
|
||||
var wetland = Terrain25Runtime.wetlandPlan(generator.delegate(root), generator.randomState(root), region.region());
|
||||
wetlands.add(Map.of("region", List.of(region.region().x(), region.region().z()), "sites", wetland.sites(), "candidates", wetland.candidates(), "refusals", wetland.refusals()));
|
||||
}
|
||||
row.put("wetlands", wetlands);
|
||||
for (String id : FAMILIES) if (plan.starts().stream().anyMatch(p -> p.key().identifier().toString().equals(id))) witnesses.get(id).add(seed);
|
||||
row.put("stage", "complete"); row.put("evaluation_succeeded", true);
|
||||
} catch (Exception | AssertionError failure) {
|
||||
failures[0] = true; row.put("evaluation_succeeded", false); row.put("failure", failure.toString());
|
||||
SanctuaryMod.LOGGER.error("Underground25 seed search failed for seed {} size {}", seed, diameter, failure);
|
||||
}
|
||||
boolean hostUnchanged = level.getChunkSource().getGenerator() == liveGenerator && (live == null
|
||||
|| live.source().islands().equals(liveIslands) && UndergroundStructures25.plan(live) == liveUnderground
|
||||
&& live.aerialPlan24() == liveAerial && live.mineralSanctuary23() == liveMineral
|
||||
&& live.transitPlan21() == liveTransit && live.undergroundHall22() == liveHall
|
||||
&& live.heritagePlan19() == liveHeritage && live.ruinsPlan17() == liveRuins);
|
||||
row.put("host_reservations_unchanged", hostUnchanged); row.put("elapsed_ms", millis(started));
|
||||
report.put("evaluated_seeds", seeds.subList(0, index[0]));
|
||||
report.put("unevaluated_seeds", seeds.subList(index[0], seeds.size()));
|
||||
write(Path.of("diagnostics/underground25/search/" + diameter + "-" + seed + ".json"), row); write(OUTPUT, report);
|
||||
helper.assertTrue(hostUnchanged, "Search must never modify the live world's generator, descriptors or reservations");
|
||||
// No generator, RandomState, native piece or sampler is retained in the report.
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
} catch (IOException failure) { throw new IllegalStateException("Cannot persist incremental seed-search diagnostics", failure); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
}
|
||||
|
||||
/** Terrain preselection only: build the same named native density, without any Sanctuary world attachment. */
|
||||
private static void dryOnly(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel(); var registries = level.registryAccess();
|
||||
var requested = seeds(System.getProperty(PREFIX + "seeds", "0,42,2026"), MAX_DRY_SEEDS);
|
||||
int diameter = Integer.getInteger(PREFIX + "diameter", 724);
|
||||
helper.assertTrue(diameter == 512 || diameter == 724 || diameter == 1024, "Dry search needs one supported explicit diameter");
|
||||
int players = diameter == 512 ? 5 : diameter == 724 ? 10 : 20;
|
||||
var settingsKey = ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_25_" + players));
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(settingsKey);
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
var forest = registries.lookupOrThrow(Registries.BIOME).getOrThrow(fr.koka.sanctuary.worldgen.Terrain25Biomes.key("oak_forest"));
|
||||
Object liveGenerator = level.getChunkSource().getGenerator();
|
||||
var live = liveGenerator instanceof SanctuaryChunkGenerator g ? g : null;
|
||||
var before = hostPlans(live);
|
||||
boolean stopAfterFirst = Boolean.getBoolean(PREFIX + "stopAfterFirstDry");
|
||||
Path output = Path.of("diagnostics/underground25/dry-search.json");
|
||||
var rows = new ArrayList<Map<String,Object>>(); var witnesses = new ArrayList<Long>();
|
||||
var report = new LinkedHashMap<String,Object>();
|
||||
report.put("scope", "Native generation25 density and natural dry-cavern floors only; no core secrets, aerial plans, structure admission or evaluated chunks");
|
||||
report.put("limitation", "A certified large natural floor is a preselection witness, not an admitted village: final water, reservations, complete Jigsaw graph and access still require the full planner");
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("host_seed", level.getSeed());
|
||||
report.put("diameter", diameter); report.put("noise_settings", settingsKey.identifier().toString());
|
||||
report.put("requested_seeds", requested); report.put("maximum_seeds", MAX_DRY_SEEDS);
|
||||
report.put("stop_after_first_certified_large_dry", stopAfterFirst);
|
||||
report.put("worlds", rows); report.put("certified_large_dry_witnesses", witnesses);
|
||||
report.put("evaluated_world_chunks_requested", 0); report.put("complete", false); write(output, report);
|
||||
int[] index = {0}; boolean[] failed = {false}; Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
boolean stop = stopAfterFirst && !witnesses.isEmpty();
|
||||
if (index[0] == requested.size() || stop) {
|
||||
report.remove("current_seed"); report.put("complete", true);
|
||||
report.put("termination", stop ? "certified_large_dry_found" : "requested_seeds_exhausted");
|
||||
report.put("evaluated_seeds", requested.subList(0,index[0])); report.put("unevaluated_seeds", requested.subList(index[0],requested.size()));
|
||||
report.put("all_evaluations_succeeded", !failed[0]); write(output,report);
|
||||
helper.assertTrue(!failed[0], "All dry-only evaluations must finish; inspect incremental dry-search.json");
|
||||
helper.succeed(); return;
|
||||
}
|
||||
long seed = requested.get(index[0]++), start = System.nanoTime();
|
||||
var row = new LinkedHashMap<String,Object>(); row.put("seed", seed); row.put("diameter",diameter);
|
||||
row.put("stage","native_density"); rows.add(row); report.put("current_seed",seed); write(output,report);
|
||||
try {
|
||||
// This is the same settings/RandomState construction as SanctuaryChunkGenerator.context.
|
||||
// Density and natural-cavern fitting do not consult the biome source or loaded world chunks.
|
||||
var generator = new net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator(
|
||||
new net.minecraft.world.level.biome.FixedBiomeSource(forest),settings);
|
||||
var random = net.minecraft.world.level.levelgen.RandomState.create(noises,seed,settings.value());
|
||||
var view = Terrain25Runtime.view(generator,random); var plan = view.plan();
|
||||
helper.assertTrue(random.seed() == seed, "Every native density uses the explicit searched world seed");
|
||||
var chambers = plan.chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.DRY)
|
||||
.sorted(java.util.Comparator.comparingDouble((Terrain25Shape.Chamber c) -> c.rx()*c.rz()).reversed().thenComparingInt(Terrain25Shape.Chamber::id)).toList();
|
||||
var largest = chambers.getFirst(); var fit = plan.dryFits().get(largest.id());
|
||||
row.put("shape_seed",plan.seed()); row.put("terrain_radius",plan.radius()); row.put("largest_dry_id",largest.id());
|
||||
row.put("largest_dry_certified",fit != null && fit.certified());
|
||||
row.put("dry_chambers",chambers.stream().map(c -> Map.of("id",c.id(),"center",List.of(c.x(),c.y(),c.z()),
|
||||
"radii",List.of(c.rx(),c.ry(),c.rz()),"rock_floor_y",c.floorY(),"player_floor_y",c.floorY()+1,
|
||||
"dry_fit",plan.dryFits().get(c.id()),"central_candidate_chunks",UndergroundPlanner25.centralVillageChunks(c).stream().map(v -> List.of(v.x(),v.z())).toList())).toList());
|
||||
if (fit != null && fit.certified()) witnesses.add(seed);
|
||||
row.put("stage","complete"); row.put("evaluation_succeeded",true);
|
||||
SanctuaryMod.LOGGER.info("Underground25 dry-only seed {} size {} largest {} floor {} fit {} in {}ms",
|
||||
seed,diameter,largest.id(),largest.floorY(),fit,millis(start));
|
||||
} catch (Exception | AssertionError failure) {
|
||||
failed[0]=true; row.put("evaluation_succeeded",false); row.put("failure",failure.toString());
|
||||
SanctuaryMod.LOGGER.error("Underground25 dry-only seed {} failed",seed,failure);
|
||||
}
|
||||
boolean unchanged = level.getChunkSource().getGenerator() == liveGenerator && sameHostPlans(before, hostPlans(live));
|
||||
row.put("host_reservations_unchanged",unchanged); row.put("elapsed_ms",millis(start));
|
||||
report.put("evaluated_seeds",requested.subList(0,index[0])); report.put("unevaluated_seeds",requested.subList(index[0],requested.size()));
|
||||
write(Path.of("diagnostics/underground25/dry-search/"+diameter+"-"+seed+".json"),row); write(output,report);
|
||||
helper.assertTrue(unchanged,"Dry-only search must not change host descriptors or reservations");
|
||||
helper.runAtTickTime(helper.getTick()+1,() -> step[0].run());
|
||||
} catch (IOException failure) { throw new IllegalStateException("Cannot write incremental dry-only diagnostics",failure); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick()+1,() -> step[0].run());
|
||||
}
|
||||
private static List<Object> hostPlans(SanctuaryChunkGenerator live) {
|
||||
if (live == null) return List.of();
|
||||
// ArrayList permits a null optional plan while retaining all pre-existing plan identities.
|
||||
var result = new ArrayList<Object>(); result.add(List.copyOf(live.source().islands()));
|
||||
result.add(UndergroundStructures25.plan(live)); result.add(live.aerialPlan24()); result.add(live.mineralSanctuary23());
|
||||
result.add(live.transitPlan21()); result.add(live.undergroundHall22()); result.add(live.heritagePlan19()); result.add(live.ruinsPlan17());
|
||||
return result;
|
||||
}
|
||||
private static boolean sameHostPlans(List<Object> before, List<Object> after) {
|
||||
if (before.size() != after.size()) return false;
|
||||
if (before.isEmpty()) return true;
|
||||
// Descriptor contents are copied; plan objects must retain their exact identity.
|
||||
if (!before.getFirst().equals(after.getFirst())) return false;
|
||||
for (int i = 1; i < before.size(); i++) if (before.get(i) != after.get(i)) return false;
|
||||
return true;
|
||||
}
|
||||
static List<Long> seeds(String value) { return seeds(value,MAX_SEEDS); }
|
||||
private static List<Long> seeds(String value,int maximum) {
|
||||
String[] parts = value.split(",", -1);
|
||||
if (parts.length < 1 || parts.length > maximum) throw new IllegalArgumentException("Seed search accepts 1–"+maximum+" explicit signed integer seeds");
|
||||
var result = new LinkedHashSet<Long>();
|
||||
for (String part : parts) {
|
||||
long seed = Long.parseLong(part.trim());
|
||||
if (!result.add(seed)) throw new IllegalArgumentException("Duplicate seed in bounded search: " + seed);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static List<String> families(String value) {
|
||||
if (value.isBlank()) return List.of();
|
||||
var result = new LinkedHashSet<String>();
|
||||
for (String part : value.split(",", -1)) {
|
||||
String id = switch (part.trim()) {
|
||||
case "village" -> "sanctuary:underground_abandoned_village";
|
||||
case "miner_cabin", "cabin" -> "sanctuary:miner_cabin";
|
||||
case "mineshaft", "mine" -> "minecraft:mineshaft";
|
||||
case "swamp_hut", "hut" -> "minecraft:swamp_hut";
|
||||
default -> part.trim();
|
||||
};
|
||||
if (!FAMILIES.contains(id)) throw new IllegalArgumentException("Unknown conditional family: " + id);
|
||||
result.add(id);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
private static long millis(long start) { return (System.nanoTime() - start) / 1_000_000; }
|
||||
private static List<Integer> position(BlockPos p) { return List.of(p.getX(), p.getY(), p.getZ()); }
|
||||
private static List<Integer> bounds(BoundingBox b) { return List.of(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ()); }
|
||||
private static void write(Path path, Object value) throws IOException {
|
||||
Files.createDirectories(path.toAbsolutePath().getParent());
|
||||
Path temporary = path.resolveSibling(path.getFileName() + ".tmp");
|
||||
Files.writeString(temporary, JSON.toJson(value) + "\n");
|
||||
try { Files.move(temporary, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); }
|
||||
catch (AtomicMoveNotSupportedException unsupported) { Files.move(temporary, path, StandardCopyOption.REPLACE_EXISTING); }
|
||||
}
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Shape;
|
||||
import fr.koka.sanctuary.worldgen.underground25.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.entity.monster.zombie.ZombieVillager;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.vehicle.minecart.MinecartChest;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Conditional starts are inspected only when admitted; required-family fixtures can assert positive witnesses. */
|
||||
public final class Underground25WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final BlockPos VILLAGE_EXTRACTION = new BlockPos(1608, 360, 1608);
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void nativeUndergroundLifecycle(GameTestHelper helper) throws Exception {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 25, "Use a disposable generation25 world");
|
||||
var plan = UndergroundStructures25.plan(generator);
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
String phase = System.getProperty("sanctuary.test.underground25.phase", "create");
|
||||
boolean reload = phase.equals("reload");
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("phase", phase); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("diagnostics", plan.diagnostics()); report.put("columns", plan.columns());
|
||||
var descriptors = plan.starts().stream().map(p -> Map.of("id", p.key().identifier().toString(),
|
||||
"chunk", List.of(p.start().getChunkPos().x(), p.start().getChunkPos().z()), "entry", position(p.entrance()),
|
||||
"pieces", p.start().getPieces().size())).toList();
|
||||
report.put("planned_starts", descriptors);
|
||||
report.put("planned_start_bounds", plan.starts().stream().map(p -> Map.of("id", p.key().identifier().toString(),
|
||||
"chunk", List.of(p.start().getChunkPos().x(), p.start().getChunkPos().z()), "bounds", bounds(p.start().getBoundingBox()))).toList());
|
||||
report.put("planned_rooms", plan.rooms().stream().map(p -> Map.of("entry", position(p.getLocatorPosition()), "seed", p.seed())).toList());
|
||||
helper.assertTrue(plan.columns() <= UndergroundPlanner25.MAX_COLUMNS * 4, "Every stage keeps a finite, independent terrain budget");
|
||||
var types = plan.starts().stream().map(p -> p.key().identifier().toString()).toList();
|
||||
for (String required : System.getProperty("sanctuary.test.underground25.required", "").split(",")) if (!required.isBlank())
|
||||
helper.assertTrue(required.equals("rooms") ? !plan.rooms().isEmpty() : types.contains(required), "Required positive witness " + required);
|
||||
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
|
||||
helper.assertTrue(plan.starts().isEmpty() && plan.rooms().isEmpty(), "OFF adds no conditional starts or feature attempts");
|
||||
report.put("passed", true); write(Path.of("diagnostics/underground25/off.json"), report); helper.succeed(); return;
|
||||
}
|
||||
var root = generator.source().islands().stream().filter(fr.koka.sanctuary.expansion.ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var shape = Terrain25Runtime.shape(generator.delegate(root), generator.randomState(root));
|
||||
report.put("dry_chambers", shape.chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.DRY).map(c -> {
|
||||
var chamber = new LinkedHashMap<String, Object>();
|
||||
chamber.put("id", c.id()); chamber.put("center", List.of(c.x(), c.y(), c.z()));
|
||||
chamber.put("radii", List.of(c.rx(), c.ry(), c.rz())); chamber.put("rock_floor_y", c.floorY());
|
||||
chamber.put("village_standing_y", c.floorY() + 1);
|
||||
chamber.put("dry_fit", shape.dryFits().get(c.id()));
|
||||
chamber.put("floor_footprint", "Only columns with chamber.cut(x, rock_floor_y + 1, z) > 0 are certified; native graph admission is separate");
|
||||
return chamber;
|
||||
}).toList());
|
||||
if (Boolean.getBoolean("sanctuary.test.underground25.planOnly")) {
|
||||
report.put("plan_only", true); report.put("passed", true); write(Path.of("diagnostics/underground25/plan.json"), report); helper.succeed(); return;
|
||||
}
|
||||
Path world = level.getServer().getWorldPath(LevelResource.ROOT), previousPath = world.resolve("underground25-create.json");
|
||||
JsonObject previous = reload ? JsonParser.parseString(Files.readString(previousPath)).getAsJsonObject() : null;
|
||||
if (reload) {
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload uses a different JVM");
|
||||
helper.assertTrue(previous.get("planned_starts").equals(JSON.toJsonTree(descriptors)), "Conditional admissions replay identically");
|
||||
helper.assertTrue(previous.get("planned_rooms").equals(JSON.toJsonTree(report.get("planned_rooms"))), "Feature attempts replay identically");
|
||||
}
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
plan.starts().forEach(p -> p.start().getPieces().forEach(piece -> piece.getBoundingBox().intersectingChunks().forEach(chunks::add)));
|
||||
plan.rooms().forEach(p -> p.getBoundingBox().intersectingChunks().forEach(chunks::add));
|
||||
int[] wait = {0}; boolean[] tickets = {false};
|
||||
var entityChunks = new HashSet<ChunkPos>();
|
||||
plan.starts().stream().filter(p -> p.key().equals(UndergroundStructures25.VILLAGE)
|
||||
|| p.key().identifier().toString().equals("minecraft:swamp_hut") || p.key().identifier().toString().equals("minecraft:mineshaft"))
|
||||
.forEach(p -> p.start().getPieces().forEach(piece -> piece.getBoundingBox().intersectingChunks().forEach(entityChunks::add)));
|
||||
if (types.contains(UndergroundStructures25.VILLAGE.identifier().toString())) {
|
||||
// Native inhabitants can walk over a piece boundary while the remaining pieces load.
|
||||
plan.starts().stream().filter(p -> p.key().equals(UndergroundStructures25.VILLAGE)).forEach(p ->
|
||||
p.start().getBoundingBox().inflatedBy(16).intersectingChunks().forEach(entityChunks::add));
|
||||
entityChunks.add(new ChunkPos(VILLAGE_EXTRACTION.getX() >> 4, VILLAGE_EXTRACTION.getZ() >> 4));
|
||||
chunks.addAll(entityChunks);
|
||||
}
|
||||
var iterator = chunks.iterator();
|
||||
report.put("expected_full_chunks", chunks.size()); report.put("entity_ticket_chunks", entityChunks.size());
|
||||
Runnable release = () -> { if (tickets[0]) entityChunks.forEach(c -> ChunkTickets25.remove(level, c)); tickets[0] = false; };
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var c = iterator.next(); level.getChunk(c.x(), c.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!tickets[0]) { entityChunks.forEach(c -> ChunkTickets25.add(level, c)); tickets[0] = true; }
|
||||
boolean ready = entityChunks.stream().allMatch(c -> level.areEntitiesLoaded(c.pack()) && level.areEntitiesActuallyLoadedAndTicking(c));
|
||||
if (!ready && wait[0]++ < 6000) { helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return; }
|
||||
helper.assertTrue(ready, "Native occupants are inspected after all their chunks are ticking");
|
||||
var tags = new TreeMap<String, String>();
|
||||
var normalizedBounds = new ArrayList<Map<String, Object>>();
|
||||
var nativeContents = new TreeMap<String, Object>();
|
||||
var summaries = new ArrayList<Map<String, Object>>();
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
for (var planned : plan.starts()) {
|
||||
var c = planned.start().getChunkPos();
|
||||
var start = level.getChunk(c.x(), c.z(), ChunkStatus.FULL, true).getStartForStructure(planned.start().getStructure());
|
||||
helper.assertTrue(start != null && start.isValid(), "Admitted site has a real native start");
|
||||
String startKey = planned.key().identifier() + "/" + c.x() + "/" + c.z();
|
||||
var canonicalTag = roundTripNative(helper, start, context, c, level.getSeed(), startKey, normalizedBounds);
|
||||
tags.put(startKey, canonicalTag.toString());
|
||||
var summary = new LinkedHashMap<String, Object>(); summary.put("id", planned.key().identifier().toString()); summary.put("pieces", start.getPieces().size());
|
||||
if (planned.key().identifier().toString().equals("minecraft:mineshaft")) inspectMine(helper, level, start, summary, nativeContents);
|
||||
if (planned.key().equals(UndergroundStructures25.VILLAGE)) {
|
||||
var mutations = inspectVillage(helper, level, start, summary, nativeContents, reload,
|
||||
previous == null ? null : previous.getAsJsonObject("village_mutations"));
|
||||
report.put("village_mutations", mutations);
|
||||
}
|
||||
if (planned.key().identifier().toString().equals("minecraft:swamp_hut")) {
|
||||
var hutTag = start.getPieces().getFirst().createTag(context);
|
||||
helper.assertTrue(hutTag.getBooleanOr("Witch", false) && hutTag.getBooleanOr("Cat", false), "Native hut places and saves its one-time witch and cat");
|
||||
summary.put("native_witch_and_cat_placed", true);
|
||||
}
|
||||
if (planned.key().equals(UndergroundStructures25.MINER_CABIN)) {
|
||||
var cabin = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.MINER_CABIN)
|
||||
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
|
||||
var approach = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.ACCESS)
|
||||
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
|
||||
helper.assertTrue(plan.starts().stream().filter(p -> p.key().identifier().toString().equals("minecraft:mineshaft"))
|
||||
.anyMatch(m -> m.start().getPieces().stream().anyMatch(p -> approach.volumes().stream().anyMatch(v -> v.intersects(p.getBoundingBox())))),
|
||||
"The cabin approach reaches a piece of an actual admitted native mineshaft");
|
||||
helper.assertTrue(level.getBlockState(cabin.chestPosition()).is(Blocks.CHEST), "Attached miner cabin has a real chest");
|
||||
helper.assertTrue(level.getBlockEntity(cabin.chestPosition()) instanceof RandomizableContainerBlockEntity, "Miner chest has native inventory and loot state");
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(cabin.chestPosition());
|
||||
if (!reload) { chest.unpackLootTable(null); chest.clearContent(); chest.setChanged(); }
|
||||
else helper.assertTrue(chest.isEmpty(), "Looted miner chest stays empty after reload");
|
||||
summary.put("chest", position(cabin.chestPosition()));
|
||||
}
|
||||
summaries.add(summary);
|
||||
}
|
||||
var roomContents = new ArrayList<Map<String, Object>>();
|
||||
for (var room : plan.rooms()) {
|
||||
helper.assertTrue(level.getBlockState(room.getLocatorPosition()).is(Blocks.SPAWNER), "Successful native room contains its spawner");
|
||||
if (!reload) helper.assertTrue(room.featureAttempted() && room.featurePlaced(), "Admitted room passes the real vanilla doorway check");
|
||||
var roomReport = new LinkedHashMap<String, Object>();
|
||||
roomReport.put("entry", position(room.getLocatorPosition()));
|
||||
roomReport.put("spawner", inspectSpawner(helper, level, room.getLocatorPosition(),
|
||||
Set.of("minecraft:skeleton", "minecraft:zombie", "minecraft:spider"), nativeContents));
|
||||
var chests = new ArrayList<Map<String, Object>>();
|
||||
for (var pos : positions(room.getBoundingBox())) if (level.getBlockState(pos).is(Blocks.CHEST))
|
||||
chests.add(inspectChest(helper, level, pos, BuiltInLootTables.SIMPLE_DUNGEON, nativeContents));
|
||||
roomReport.put("optional_chests", chests); roomContents.add(roomReport);
|
||||
}
|
||||
if (reload) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "No start, piece or one-time native flag changes on reload");
|
||||
if (reload && previous.has("native_contents")) {
|
||||
helper.assertTrue(previous.get("native_contents").equals(JSON.toJsonTree(nativeContents)),
|
||||
"Native unopened loot tables/seeds, minecart UUIDs and spawner configurations survive reload");
|
||||
report.put("native_contents_reload_compared", true);
|
||||
} else report.put("native_contents_reload_compared", false);
|
||||
report.put("native_jigsaw_bounds_normalized", normalizedBounds);
|
||||
report.put("native_jigsaw_bounds_normalized_count", normalizedBounds.size());
|
||||
report.put("native_tag_comparison", "All fields are strict; only native Jigsaw expanded maxY may normalize once to the exact template box. Second decode is entirely strict.");
|
||||
report.put("native_contents", nativeContents); report.put("actual_monster_rooms", roomContents);
|
||||
report.put("actual_starts", summaries); report.put("native_tags", tags); report.put("full_chunks", chunks.size());
|
||||
report.put("native_room_spawners", plan.rooms().size()); report.put("entity_wait_ticks", wait[0]);
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save native starts, blocks, entities and emptied inventories");
|
||||
report.put("passed", true); write(world.resolve("underground25-" + phase + ".json"), report);
|
||||
write(Path.of("diagnostics/underground25/" + phase + ".json"), report); release.run(); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
release.run(); report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(Path.of("diagnostics/underground25/failure.json"), report); } catch (Exception ignored) {}
|
||||
throw new IllegalStateException("Underground25 lifecycle failed", failure);
|
||||
}
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
}
|
||||
/** Vanilla JigsawPlacement expands some planning boxes upward; the native loader recomputes the template box. */
|
||||
private static net.minecraft.nbt.CompoundTag roundTripNative(GameTestHelper helper, StructureStart start,
|
||||
StructurePieceSerializationContext context, ChunkPos chunk, long seed, String startKey,
|
||||
List<Map<String, Object>> normalized) {
|
||||
var before = start.createTag(context, chunk);
|
||||
var decoded = StructureStart.loadStaticStart(context, before, seed);
|
||||
helper.assertTrue(decoded != null && decoded.isValid(), "Every native start and cavity decodes");
|
||||
var after = decoded.createTag(context, chunk);
|
||||
var beforeChildren = before.getListOrEmpty("Children");
|
||||
var afterChildren = after.getListOrEmpty("Children");
|
||||
helper.assertTrue(start.getPieces().size() == decoded.getPieces().size()
|
||||
&& beforeChildren.size() == afterChildren.size() && beforeChildren.size() == start.getPieces().size(),
|
||||
"No native or cavity piece is added, removed or reordered by decoding");
|
||||
var beforeHeader = before.copy(); beforeHeader.remove("Children");
|
||||
var afterHeader = after.copy(); afterHeader.remove("Children");
|
||||
helper.assertTrue(beforeHeader.equals(afterHeader), "Native start identity, references and header round-trip exactly");
|
||||
for (int i = 0; i < start.getPieces().size(); i++) {
|
||||
var originalPiece = start.getPieces().get(i); var decodedPiece = decoded.getPieces().get(i);
|
||||
var originalTag = beforeChildren.getCompoundOrEmpty(i); var decodedTag = afterChildren.getCompoundOrEmpty(i);
|
||||
if (originalPiece instanceof PoolElementStructurePiece pool) {
|
||||
helper.assertTrue(decodedPiece instanceof PoolElementStructurePiece, "A Jigsaw piece retains its native type");
|
||||
var originalBox = originalPiece.getBoundingBox(); var loadedBox = decodedPiece.getBoundingBox();
|
||||
var templateBox = pool.getElement().getBoundingBox(context.structureTemplateManager(), pool.getPosition(), pool.getRotation());
|
||||
helper.assertTrue(loadedBox.equals(templateBox), "Reloaded Jigsaw bounds equal the real rotated native template");
|
||||
helper.assertTrue(originalBox.minX() == loadedBox.minX() && originalBox.maxX() == loadedBox.maxX()
|
||||
&& originalBox.minZ() == loadedBox.minZ() && originalBox.maxZ() == loadedBox.maxZ()
|
||||
&& originalBox.minY() == loadedBox.minY() && loadedBox.maxY() <= originalBox.maxY(),
|
||||
"Native normalization can only shrink planning maxY, preserving position, floor and all chunk coverage");
|
||||
var originalFields = originalTag.copy(); originalFields.remove("BB");
|
||||
var decodedFields = decodedTag.copy(); decodedFields.remove("BB");
|
||||
helper.assertTrue(originalFields.equals(decodedFields),
|
||||
"All Jigsaw pool paths, positions, rotation, projection, junctions and other fields round-trip exactly");
|
||||
if (!originalBox.equals(loadedBox)) normalized.add(Map.of("start", startKey, "piece_index", i,
|
||||
"element", pool.getElement().toString(), "original", bounds(originalBox), "native_template", bounds(loadedBox)));
|
||||
} else helper.assertTrue(originalTag.equals(decodedTag),
|
||||
"Cavity volumes, protection, accesses and all non-Jigsaw native fields round-trip exactly");
|
||||
}
|
||||
helper.assertTrue(start.getBoundingBox().equals(decoded.getBoundingBox()), "The complete admitted structure and protected cavity keep the same envelope");
|
||||
var twice = StructureStart.loadStaticStart(context, after, seed);
|
||||
helper.assertTrue(twice != null && after.equals(twice.createTag(context, chunk)), "Second native decode is entirely idempotent, including every bounding box");
|
||||
return after;
|
||||
}
|
||||
private static void inspectMine(GameTestHelper helper, ServerLevel level, StructureStart start,
|
||||
Map<String, Object> report, Map<String, Object> persistent) {
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var flags = new ArrayList<Map<String, Object>>();
|
||||
var blocks = new TreeMap<String, Integer>();
|
||||
var spawners = new ArrayList<Map<String, Object>>();
|
||||
var chests = new ArrayList<Map<String, Object>>();
|
||||
var railPositions = new ArrayList<List<Integer>>();
|
||||
var visited = new HashSet<BlockPos>();
|
||||
var coreVolumes = start.getPieces().stream().map(p -> p.getBoundingBox()).toList();
|
||||
for (var piece : start.getPieces()) {
|
||||
if (piece instanceof MineshaftPieces.MineShaftCorridor) {
|
||||
var tag = piece.createTag(context);
|
||||
boolean rails = tag.getBooleanOr("hr", false), spiders = tag.getBooleanOr("sc", false), placedSpider = tag.getBooleanOr("hps", false);
|
||||
helper.assertTrue(!(rails && spiders) && (!placedSpider || spiders), "Native corridor rail/spider flags retain their vanilla invariants");
|
||||
flags.add(Map.of("bounds", bounds(piece.getBoundingBox()), "has_rails", rails,
|
||||
"spider_corridor", spiders, "placed_spider_spawner", placedSpider, "sections", tag.getIntOr("Num", 0)));
|
||||
}
|
||||
// Include the certified native support envelope. Deduplicate overlaps between pieces.
|
||||
var volume = UndergroundPlacement25.writeBounds("minecraft:mineshaft", piece.getBoundingBox());
|
||||
for (var at : positions(volume)) {
|
||||
var pos = at.immutable(); if (!visited.add(pos)) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (state.is(Blocks.RAIL) || state.is(Blocks.COBWEB) || state.is(Blocks.OAK_PLANKS)
|
||||
|| state.is(Blocks.OAK_FENCE) || state.is(Blocks.OAK_LOG) || state.is(Blocks.DARK_OAK_PLANKS)
|
||||
|| state.is(Blocks.DARK_OAK_FENCE) || state.is(Blocks.DARK_OAK_LOG)) {
|
||||
blocks.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
if (state.is(Blocks.RAIL)) {
|
||||
helper.assertTrue(state.canSurvive(level, pos), "Every observed mine rail has its real vanilla support at " + pos);
|
||||
railPositions.add(position(pos));
|
||||
}
|
||||
}
|
||||
// Do not attribute a nearby native dungeon below a support column to this mine.
|
||||
if ((!state.is(Blocks.SPAWNER) && !state.is(Blocks.CHEST)) || !coreVolumes.stream().anyMatch(b -> b.isInside(pos))) continue;
|
||||
if (state.is(Blocks.SPAWNER)) spawners.add(inspectSpawner(helper, level, pos, Set.of("minecraft:cave_spider"), persistent));
|
||||
if (state.is(Blocks.CHEST)) chests.add(inspectChest(helper, level, pos, BuiltInLootTables.ABANDONED_MINESHAFT, persistent));
|
||||
}
|
||||
}
|
||||
var box = start.getBoundingBox();
|
||||
var carts = level.getEntitiesOfClass(MinecartChest.class,
|
||||
new AABB(box.minX(), box.minY(), box.minZ(), box.maxX() + 1, box.maxY() + 1, box.maxZ() + 1));
|
||||
var minecarts = new ArrayList<Map<String, Object>>();
|
||||
for (var cart : carts.stream().sorted(Comparator.comparing(c -> c.getUUID().toString())).toList()) {
|
||||
var table = cart.getContainerLootTable();
|
||||
helper.assertTrue(BuiltInLootTables.ABANDONED_MINESHAFT.equals(table), "An unopened native minecart retains its mineshaft loot table");
|
||||
var state = Map.of("loot_table", table.identifier().toString(), "loot_seed", cart.getContainerLootTableSeed());
|
||||
persistent.put("minecart:" + cart.getUUID(), state);
|
||||
minecarts.add(Map.of("uuid", cart.getUUID().toString(), "position", List.of(cart.getX(), cart.getY(), cart.getZ()),
|
||||
"loot_table", table.identifier().toString(), "loot_seed", cart.getContainerLootTableSeed()));
|
||||
}
|
||||
report.put("mine_observation_scope", "Unique blocks in native piece/support envelopes; rail/web/chest/cart/spawner outcomes are conditional, with no minimum quota");
|
||||
report.put("native_corridor_flags", flags); report.put("observed_rail_web_wood_blocks", blocks);
|
||||
report.put("supported_rail_positions", railPositions); report.put("optional_native_spawners", spawners);
|
||||
report.put("optional_native_chests", chests); report.put("optional_native_minecarts", minecarts);
|
||||
}
|
||||
private static Map<String, Object> inspectSpawner(GameTestHelper helper, ServerLevel level, BlockPos pos,
|
||||
Set<String> permittedTypes, Map<String, Object> persistent) {
|
||||
helper.assertTrue(level.getBlockEntity(pos) instanceof SpawnerBlockEntity, "Spawner block has its real native BlockEntity at " + pos);
|
||||
var spawner = (SpawnerBlockEntity) level.getBlockEntity(pos);
|
||||
var nbt = spawner.saveWithoutMetadata(level.registryAccess());
|
||||
var spawnData = nbt.getCompound("SpawnData").orElseThrow();
|
||||
String type = spawnData.getCompound("entity").orElseThrow().getString("id").orElseThrow();
|
||||
helper.assertTrue(permittedTypes.contains(type), "Native spawner contains an allowed actual entity type at " + pos + ": " + type);
|
||||
// Delay changes with player proximity; the configured type, potentials and limits must persist.
|
||||
var configuration = nbt.copy(); configuration.remove("Delay");
|
||||
var limits = new TreeMap<String, Integer>();
|
||||
for (String key : List.of("MinSpawnDelay", "MaxSpawnDelay", "SpawnCount", "MaxNearbyEntities", "RequiredPlayerRange", "SpawnRange"))
|
||||
limits.put(key, nbt.getIntOr(key, -1));
|
||||
persistent.put("spawner:" + position(pos), Map.of("entity_type", type, "limits", limits));
|
||||
return Map.of("position", position(pos), "entity_type", type, "configuration_nbt", configuration.toString(), "nbt", nbt.toString());
|
||||
}
|
||||
private static Map<String, Object> inspectChest(GameTestHelper helper, ServerLevel level, BlockPos pos,
|
||||
net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> expected,
|
||||
Map<String, Object> persistent) {
|
||||
helper.assertTrue(level.getBlockEntity(pos) instanceof RandomizableContainerBlockEntity, "Native chest has its inventory BlockEntity at " + pos);
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(pos);
|
||||
var table = chest.getLootTable();
|
||||
helper.assertTrue(expected.equals(table), "An unopened native chest retains its structure's loot table at " + pos);
|
||||
var state = Map.of("loot_table", table.identifier().toString(), "loot_seed", chest.getLootTableSeed());
|
||||
persistent.put("chest:" + position(pos), state);
|
||||
return Map.of("position", position(pos), "loot_table", table.identifier().toString(), "loot_seed", chest.getLootTableSeed());
|
||||
}
|
||||
private static Object inspectVillage(GameTestHelper helper, ServerLevel level, StructureStart start,
|
||||
Map<String, Object> report, Map<String, Object> persistent, boolean reload, JsonObject previousMutations) {
|
||||
var cavern = start.getPieces().stream().filter(p -> p instanceof UndergroundPiece25 u && u.kind() == UndergroundPiece25.Kind.VILLAGE_CAVERN)
|
||||
.map(UndergroundPiece25.class::cast).findFirst().orElseThrow();
|
||||
helper.assertTrue(start.getPieces().getFirst() == cavern, "Cavity is placed before native roads and buildings");
|
||||
int houses = 0, paths = 0;
|
||||
var roadWitnesses = new ArrayList<BlockPos>();
|
||||
var chests = new TreeSet<BlockPos>(Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ));
|
||||
var visited = new HashSet<BlockPos>();
|
||||
for (var piece : start.getPieces()) if (piece instanceof PoolElementStructurePiece pool) {
|
||||
String template = pool.getElement().toString();
|
||||
if (template.contains("/houses/")) houses++;
|
||||
for (var at : positions(piece.getBoundingBox())) if (level.getBlockState(at).is(Blocks.CHEST)) chests.add(at.immutable());
|
||||
if (!template.contains("/streets/")) continue;
|
||||
var b = piece.getBoundingBox();
|
||||
for (int z = b.minZ(); z <= b.maxZ(); z++) for (int x = b.minX(); x <= b.maxX(); x++) {
|
||||
var pos = new BlockPos(x, cavern.floorY() - 1, z);
|
||||
if (visited.add(pos) && level.getBlockState(pos).is(Blocks.DIRT_PATH)) {
|
||||
helper.assertTrue(level.getBlockState(pos.below()).isSolid(), "Native underground road has a foundation"); paths++; roadWitnesses.add(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(houses >= 6, "An extended native village, not a truncated hamlet");
|
||||
helper.assertTrue(paths > 10, "Native streets actually project to the saved underground floor");
|
||||
var entry = cavern.getLocatorPosition();
|
||||
helper.assertTrue(level.getBlockState(entry.below()).isSolid() && level.getBlockState(entry).getCollisionShape(level, entry).isEmpty()
|
||||
&& level.getBlockState(entry.above()).getCollisionShape(level, entry.above()).isEmpty(), "Village access is supported and passable");
|
||||
var b = start.getBoundingBox();
|
||||
var search = new AABB(b.minX() - 16, b.minY() - 8, b.minZ() - 16, b.maxX() + 17, b.maxY() + 17, b.maxZ() + 17);
|
||||
var zombies = new ArrayList<>(level.getEntitiesOfClass(ZombieVillager.class, search, v -> v.isAlive() && v.isPersistenceRequired()));
|
||||
if (reload) {
|
||||
helper.assertTrue(previousMutations != null, "Reload keeps the recorded village edits and extracted occupant");
|
||||
var extracted = level.getEntity(UUID.fromString(previousMutations.get("extracted_uuid").getAsString()));
|
||||
helper.assertTrue(extracted instanceof ZombieVillager && extracted.isAlive(), "The extracted zombie-villager reloads under the same UUID");
|
||||
helper.assertTrue(!search.contains(extracted.position()), "The extracted occupant stays outside its original village");
|
||||
zombies.add((ZombieVillager) extracted);
|
||||
}
|
||||
zombies.sort(Comparator.comparing(v -> v.getUUID().toString()));
|
||||
helper.assertTrue(!zombies.isEmpty(), "Native abandoned templates place persistent zombie-villagers");
|
||||
var identities = new TreeMap<String, Object>();
|
||||
for (var zombie : zombies) {
|
||||
helper.assertTrue(zombie.getType() == EntityTypes.ZOMBIE_VILLAGER && zombie.isPersistenceRequired() && !zombie.isConverting(),
|
||||
"Native persistent zombie-villagers retain their ordinary curable type, without a scripted conversion");
|
||||
helper.assertTrue(!identities.containsKey(zombie.getUUID().toString()), "No duplicate live village UUID");
|
||||
var data = zombie.getVillagerData();
|
||||
identities.put(zombie.getUUID().toString(), Map.of("persistent", true,
|
||||
"villager_type", data.type().unwrapKey().orElseThrow().identifier().toString(),
|
||||
"profession", data.profession().unwrapKey().orElseThrow().identifier().toString(), "level", data.level()));
|
||||
}
|
||||
persistent.put("village:" + start.getChunkPos().pack(), identities);
|
||||
Object mutations;
|
||||
if (!reload) {
|
||||
var chestPos = chests.stream().filter(pos -> level.getBlockEntity(pos) instanceof RandomizableContainerBlockEntity chest
|
||||
&& chest.getLootTable() != null && chest.getLootTable().identifier().getPath().startsWith("chests/village/")).findFirst().orElse(null);
|
||||
helper.assertTrue(chestPos != null, "The positive village witness contains an actual native village-loot chest");
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestPos);
|
||||
String table = chest.getLootTable().identifier().toString(); long lootSeed = chest.getLootTableSeed();
|
||||
chest.unpackLootTable(null); var loot = new TreeMap<String, Integer>();
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
var stack = chest.getItem(slot);
|
||||
if (!stack.isEmpty()) loot.merge(BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(), stack.getCount(), Integer::sum);
|
||||
}
|
||||
chest.clearContent(); chest.setChanged();
|
||||
var removed = roadWitnesses.stream().filter(pos -> pos.distSqr(entry.below()) > 16).findFirst().orElseThrow();
|
||||
helper.assertTrue(level.setBlock(removed, Blocks.AIR.defaultBlockState(), 2), "Remove one real native road block");
|
||||
helper.assertTrue(!search.contains(VILLAGE_EXTRACTION.getX() + .5, VILLAGE_EXTRACTION.getY(), VILLAGE_EXTRACTION.getZ() + .5),
|
||||
"The test holding cell is outside the village");
|
||||
for (int dz = -2; dz <= 2; dz++) for (int dx = -2; dx <= 2; dx++) for (int dy = -1; dy <= 3; dy++) {
|
||||
var at = VILLAGE_EXTRACTION.offset(dx, dy, dz);
|
||||
boolean shell = Math.abs(dx) == 2 || Math.abs(dz) == 2 || dy == -1 || dy == 3;
|
||||
level.setBlock(at, (shell ? Blocks.STONE : Blocks.AIR).defaultBlockState(), 2);
|
||||
}
|
||||
var extracted = zombies.getFirst();
|
||||
// Only this disposable test occupant is immobilized, so reload checks do not depend on AI wandering.
|
||||
extracted.setNoAi(true); extracted.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
extracted.snapTo(VILLAGE_EXTRACTION.getX() + .5, VILLAGE_EXTRACTION.getY(), VILLAGE_EXTRACTION.getZ() + .5);
|
||||
mutations = Map.of("emptied_chest", position(chestPos), "original_loot_table", table, "original_loot_seed", lootSeed,
|
||||
"loot_items_obtained", loot, "removed_road_block", position(removed), "extracted_uuid", extracted.getUUID().toString(),
|
||||
"extracted_position", List.of(extracted.getX(), extracted.getY(), extracted.getZ()),
|
||||
"extraction_fixture", "Covered holding cell in a distant disposable-world chunk; only the extracted occupant has NoAI");
|
||||
} else {
|
||||
var chestPos = position(previousMutations.getAsJsonArray("emptied_chest"));
|
||||
helper.assertTrue(level.getBlockEntity(chestPos) instanceof RandomizableContainerBlockEntity,
|
||||
"The opened village chest remains the same native container after reload");
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestPos);
|
||||
helper.assertTrue(chest.isEmpty() && chest.getLootTable() == null, "Looted village chest stays empty without rerolling its native loot");
|
||||
var removed = position(previousMutations.getAsJsonArray("removed_road_block"));
|
||||
helper.assertTrue(level.getBlockState(removed).isAir(), "A demolished native road block is not reconstructed on reload");
|
||||
var extracted = level.getEntity(UUID.fromString(previousMutations.get("extracted_uuid").getAsString()));
|
||||
var at = previousMutations.getAsJsonArray("extracted_position");
|
||||
helper.assertTrue(Math.abs(extracted.getX() - at.get(0).getAsDouble()) < .01
|
||||
&& Math.abs(extracted.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(extracted.getZ() - at.get(2).getAsDouble()) < .01,
|
||||
"The extracted zombie-villager retains its recorded position and is not replenished in the village");
|
||||
mutations = previousMutations.deepCopy();
|
||||
}
|
||||
report.put("houses", houses); report.put("projected_path_blocks", paths);
|
||||
report.put("zombie_villagers", identities.keySet()); report.put("persistent_native_zombie_villagers", identities.size());
|
||||
report.put("curability_scope", "Native ZombieVillager type and persistence checked; no complete cure is claimed");
|
||||
report.put("floor", cavern.floorY()); report.put("entry", position(entry));
|
||||
return mutations;
|
||||
}
|
||||
private static List<Integer> position(BlockPos p) { return List.of(p.getX(), p.getY(), p.getZ()); }
|
||||
private static BlockPos position(com.google.gson.JsonArray p) { return new BlockPos(p.get(0).getAsInt(), p.get(1).getAsInt(), p.get(2).getAsInt()); }
|
||||
private static Iterable<BlockPos> positions(BoundingBox b) {
|
||||
return BlockPos.betweenClosed(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ());
|
||||
}
|
||||
private static List<Integer> bounds(net.minecraft.world.level.levelgen.structure.BoundingBox b) {
|
||||
return List.of(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ());
|
||||
}
|
||||
private static void write(Path path, Object value) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(value) + "\n"); }
|
||||
}
|
||||
+31
-9
@@ -107,7 +107,7 @@ public final class UnifiedWorldGameTests {
|
||||
for (int y = 0; y < 256; y++) {
|
||||
var pos = new BlockPos(x, y, z);
|
||||
var state = chunk.getBlockState(pos);
|
||||
String biome = chunk.getNoiseBiome(x >> 2, y >> 2, z >> 2).getRegisteredName();
|
||||
String biome = caveFamily(binding, chunk.getNoiseBiome(x >> 2, y >> 2, z >> 2).getRegisteredName());
|
||||
var bucket = counts.get(biome);
|
||||
if (bucket != null && !state.isAir()) bucket.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1L, Long::sum);
|
||||
// Large vanilla pillars add thick dripstone into original cave air. A painted
|
||||
@@ -129,8 +129,9 @@ public final class UnifiedWorldGameTests {
|
||||
Map<String, Object> atmospheres = new TreeMap<>();
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
for (String name : CAVES) {
|
||||
String actualId = caveId(binding, name);
|
||||
var biome = level.registryAccess().lookupOrThrow(Registries.BIOME)
|
||||
.getValue(net.minecraft.resources.Identifier.parse(name));
|
||||
.getValue(net.minecraft.resources.Identifier.parse(actualId));
|
||||
var encoded = Biome.DIRECT_CODEC.encodeStart(ops, biome).getOrThrow().getAsJsonObject();
|
||||
atmospheres.put(name, encoded.get("attributes"));
|
||||
helper.assertTrue(encoded.get("attributes").toString().contains("music.overworld." + name.substring("minecraft:".length())),
|
||||
@@ -164,8 +165,9 @@ public final class UnifiedWorldGameTests {
|
||||
"Actual native sulfur caverns must include native rock and spikes");
|
||||
verified.add("minecraft:sulfur_caves");
|
||||
}
|
||||
helper.assertTrue(surfaceBiomes.keySet().stream().anyMatch(name -> name.startsWith("sanctuary:population_")),
|
||||
"The starter surface must retain its Population biome family above these lower cave patches");
|
||||
String surfacePrefix = generator.rootGeneration() == 25 ? "sanctuary:terrain25_" : "sanctuary:population_";
|
||||
helper.assertTrue(surfaceBiomes.keySet().stream().anyMatch(name -> name.startsWith(surfacePrefix)),
|
||||
"The starter surface must retain its generation-specific woodland biome family above these lower cave patches");
|
||||
passed = true; helper.succeed();
|
||||
} finally {
|
||||
report.put("encountered_native_types", encountered); report.put("verified_native_types", verified);
|
||||
@@ -248,7 +250,7 @@ public final class UnifiedWorldGameTests {
|
||||
&& chunk.getBlockState(free).is(Blocks.DIAMOND_BLOCK),
|
||||
"Decoration outside protected volumes must remain writable");
|
||||
} finally { chunk.setBlockState(free, freeBefore, 0); }
|
||||
write(level, "decoration-guard", Map.of("generation", 13, "seed", level.getSeed(),
|
||||
write(level, "decoration-guard", Map.of("generation", generator.rootGeneration(), "seed", level.getSeed(),
|
||||
"diameter", generator.initialDiameter(), "compared_plan_points", compared,
|
||||
"natural_wall_witnesses", shells, "walls_owned_by_adjacent_chunk", boundaryWitnesses, "actual_write_rejected", true, "unprotected_write_allowed", true, "passed", true));
|
||||
helper.succeed();
|
||||
@@ -266,7 +268,7 @@ public final class UnifiedWorldGameTests {
|
||||
int x = cx * 16 + 8, z = cz * 16 + 8;
|
||||
if (Math.hypot(x, z) >= capacity.radius() - 24) continue;
|
||||
for (int y = 164; y >= 40; y--) {
|
||||
String biome = resolver.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2).getRegisteredName();
|
||||
String biome = caveFamily(binding, resolver.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2).getRegisteredName());
|
||||
var bucket = candidates.get(biome); if (bucket == null) continue;
|
||||
if (sample(binding, density, x, y, z) <= 0 || sample(binding, density, x, y + 1, z) > 0
|
||||
|| sample(binding, density, x, y + 2, z) > 0 || sample(binding, density, x, y - 1, z) <= 0) continue;
|
||||
@@ -286,8 +288,10 @@ public final class UnifiedWorldGameTests {
|
||||
.thenComparingDouble(s -> Math.hypot(s.x(), s.z())).thenComparingInt(Site::x).thenComparingInt(Site::z)).toList();
|
||||
for (Site candidate : ordered.stream().limit(32).toList()) {
|
||||
var base = new BlockPos(candidate.x(), candidate.floorY() + 1, candidate.z());
|
||||
if (!PopulationHydrologyRuntime.protects(binding.generator(), binding.random(), base, 8,
|
||||
candidate.roofY() - candidate.floorY(), 3)) { sites.add(candidate); break; }
|
||||
boolean protectedSite = Terrain25Runtime.enabled(binding.generator())
|
||||
? Terrain25Runtime.protects(binding.generator(), binding.random(), base, 8, candidate.roofY() - candidate.floorY(), 3)
|
||||
: PopulationHydrologyRuntime.protects(binding.generator(), binding.random(), base, 8, candidate.roofY() - candidate.floorY(), 3);
|
||||
if (!protectedSite) { sites.add(candidate); break; }
|
||||
}
|
||||
}
|
||||
return List.copyOf(sites);
|
||||
@@ -296,6 +300,24 @@ public final class UnifiedWorldGameTests {
|
||||
private static float sample(PopulationGenerationContext.Binding binding, DensityFunction density, int x, int y, int z) {
|
||||
return binding.random().sampleBlockValueUncached(density, x, y, z);
|
||||
}
|
||||
private static String caveFamily(PopulationGenerationContext.Binding binding, String name) {
|
||||
if (!Terrain25Runtime.enabled(binding.generator())) return name;
|
||||
return switch (name) {
|
||||
case "sanctuary:terrain25_lush_caves" -> "minecraft:lush_caves";
|
||||
case "sanctuary:terrain25_dripstone_caves" -> "minecraft:dripstone_caves";
|
||||
case "sanctuary:terrain25_sulfur_depths" -> "minecraft:sulfur_caves";
|
||||
default -> name;
|
||||
};
|
||||
}
|
||||
private static String caveId(PopulationGenerationContext.Binding binding, String name) {
|
||||
if (!Terrain25Runtime.enabled(binding.generator())) return name;
|
||||
return switch (name) {
|
||||
case "minecraft:lush_caves" -> "sanctuary:terrain25_lush_caves";
|
||||
case "minecraft:dripstone_caves" -> "sanctuary:terrain25_dripstone_caves";
|
||||
case "minecraft:sulfur_caves" -> "sanctuary:terrain25_sulfur_depths";
|
||||
default -> throw new IllegalArgumentException("Unknown cave family " + name);
|
||||
};
|
||||
}
|
||||
private static float natural(PopulationGenerationContext.Binding binding, BlockPos pos) {
|
||||
return sample(binding, binding.generator().generatorSettings().value().noiseRouter().finalDensity(), pos.getX(), pos.getY(), pos.getZ());
|
||||
}
|
||||
@@ -309,7 +331,7 @@ public final class UnifiedWorldGameTests {
|
||||
}
|
||||
private static Map<String, Object> report(ServerLevel level) {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("generation", 13); report.put("seed", level.getSeed()); report.put("diameter", unified(level).initialDiameter());
|
||||
report.put("generation", unified(level).rootGeneration()); report.put("seed", level.getSeed()); report.put("diameter", unified(level).initialDiameter());
|
||||
report.put("mod_version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().getMetadata().getVersion().getFriendlyString());
|
||||
return report;
|
||||
}
|
||||
|
||||
+37
-12
@@ -64,7 +64,10 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
var historical23 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v23")));
|
||||
helper.assertTrue(historical23.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
|
||||
&& old.rootGeneration() == 23 && old.source().generation() == 23, "Historical23 remains separately readable");
|
||||
helper.assertTrue(generator.rootGeneration() == 24 && generator.source().generation() == 24 && generator.experimentalStructures(), "The public preset uses generation24 with its new-world option");
|
||||
var historical24 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v24")));
|
||||
helper.assertTrue(historical24.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
|
||||
&& old.rootGeneration() == 24 && old.source().generation() == 24, "Historical24 remains separately readable");
|
||||
helper.assertTrue(generator.rootGeneration() == 25 && generator.source().generation() == 25 && generator.experimentalStructures(), "The public preset uses generation25 with its new-world option");
|
||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
|
||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||
@@ -75,11 +78,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var activeCodec = generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var activeCodec = generator.rootGeneration() == 25 ? SanctuaryChunkGenerator.TERRAIN25_CODEC : generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("experimental_structures");
|
||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||
"Generation24 omitted experimental option defaults to enabled");
|
||||
"Generation25 omitted experimental option defaults to enabled");
|
||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||
serialized.remove("initial_diameter");
|
||||
@@ -94,9 +97,12 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
Files.createDirectories(directory);
|
||||
var storage = LevelStorageSource.createDefault(directory);
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
|
||||
var chosen = generator.withInitialDiameter(diameter).withExperimentalStructures(experimental);
|
||||
helper.assertTrue(generator.initialDiameter() == 724 && chosen.source() != generator.source(),
|
||||
for (var generation : List.of(
|
||||
(SanctuaryChunkGenerator) historical23.value().createWorldDimensions().overworld(),
|
||||
(SanctuaryChunkGenerator) historical24.value().createWorldDimensions().overworld(), generator))
|
||||
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
|
||||
var chosen = generation.withInitialDiameter(diameter).withExperimentalStructures(experimental);
|
||||
helper.assertTrue(generation.initialDiameter() == 724 && chosen.source() != generation.source(),
|
||||
"Customizing a new world must copy the generator and its mutable biome source");
|
||||
var dimensions = standard.replaceOverworldGenerator(access, chosen);
|
||||
for (var dimension : List.of(LevelStem.NETHER, LevelStem.END)) {
|
||||
@@ -104,17 +110,19 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
"Customizing the island preserves the other dimensions");
|
||||
}
|
||||
var original = new WorldGenSettings(new WorldOptions(helper.getLevel().getSeed(), false, true), dimensions);
|
||||
String name = "diameter-" + diameter + "-experimental-" + experimental;
|
||||
String name = "generation-" + generation.rootGeneration() + "-diameter-" + diameter + "-experimental-" + experimental;
|
||||
try (var save = storage.createAccess(name)) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
helper.assertTrue(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator reloaded
|
||||
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration() && reloaded.experimentalStructures() == experimental,
|
||||
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generation.rootGeneration()
|
||||
&& reloaded.source().generation() == generation.rootGeneration() && reloaded.experimentalStructures() == experimental,
|
||||
"The selected diameter must survive real world-settings storage");
|
||||
helper.assertTrue(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
|
||||
.equals(WorldGenSettings.CODEC.encodeStart(ops, original).getOrThrow()),
|
||||
"Seed, generation options and all dimensions survive the disk round trip");
|
||||
results.add(Map.of("diameter", diameter, "experimental_structures", experimental, "disk_round_trip", true));
|
||||
results.add(Map.of("generation", generation.rootGeneration(), "diameter", diameter,
|
||||
"experimental_structures", experimental, "disk_round_trip", true));
|
||||
}
|
||||
}
|
||||
Files.writeString(directory.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
@@ -138,7 +146,7 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
|
||||
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|
||||
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24) return;
|
||||
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24 && active.rootGeneration() != 25) return;
|
||||
helper.assertTrue(!active.protectsCityDecoration(outside)
|
||||
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
||||
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
|
||||
@@ -148,11 +156,28 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
|
||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24) && helper.getLevel().structureManager().shouldGenerateStructures())
|
||||
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
|
||||
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24 || active.rootGeneration() == 25) && helper.getLevel().structureManager().shouldGenerateStructures())
|
||||
helper.assertTrue(!interior.isEmpty(), "The attached generation" + active.rootGeneration() + " world has core construction to protect");
|
||||
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
|
||||
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
|
||||
position.getX(), position.getY(), position.getZ()),
|
||||
"Real underground construction remains protected from decoration and overlapping native structures");
|
||||
if (active.rootGeneration() == 25) {
|
||||
var copied = active.source().copy();
|
||||
copied.setIslands(active.source().islands());
|
||||
var binding = active.starterContext();
|
||||
var originalResolver = active.source().createUncachedResolver(binding.random());
|
||||
var copiedResolver = copied.createUncachedResolver(binding.random());
|
||||
int checked = 0;
|
||||
for (int x = -active.initialDiameter() / 2; x <= active.initialDiameter() / 2; x += 32)
|
||||
for (int z = -active.initialDiameter() / 2; z <= active.initialDiameter() / 2; z += 32)
|
||||
for (int y : new int[]{80, 128, 160, 192, 224, 288}) {
|
||||
var expected = originalResolver.getNoiseBiome(x >> 2, y >> 2, z >> 2);
|
||||
helper.assertTrue(copiedResolver.getNoiseBiome(x >> 2, y >> 2, z >> 2).equals(expected),
|
||||
"An isolated generation25 preflight retains the exact immutable terrain-biome plan");
|
||||
checked++;
|
||||
}
|
||||
helper.assertTrue(checked > 100, "Compare copied biomes across surface and cave layers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+433
@@ -0,0 +1,433 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Biomes;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Wetlands;
|
||||
import fr.koka.sanctuary.worldgen.underground25.SwampHutAdmission25;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlan25;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.entity.animal.feline.Cat;
|
||||
import net.minecraft.world.entity.monster.Witch;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Real regional water, native FULL biomes/blocks and conditional vanilla hut occupants; no synthetic shore. */
|
||||
public final class Wetlands25WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Comparator<Terrain25Wetlands.Column> COLUMN_ORDER = Comparator
|
||||
.comparingInt(Terrain25Wetlands.Column::x).thenComparingInt(Terrain25Wetlands.Column::z)
|
||||
.thenComparingInt(Terrain25Wetlands.Column::floorY).thenComparingLong(Terrain25Wetlands.Column::waterFeature);
|
||||
private static final int WITNESSES_PER_KIND = 12, MAX_FULL_CHUNKS = 64, MAX_FLORA_COLUMNS_PER_STRATUM = 2048;
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void nativeWetlandsAndConditionalHut(GameTestHelper helper) {
|
||||
nativeLocalHutHeight(helper);
|
||||
new Run(helper).begin();
|
||||
}
|
||||
|
||||
private static void nativeLocalHutHeight(GameTestHelper helper) {
|
||||
// The native heightmap's solid-block tags exist only after server datapacks load.
|
||||
var states = new BlockState[384];
|
||||
java.util.Arrays.fill(states, Blocks.AIR.defaultBlockState());
|
||||
for (int y = 0; y <= 184; y++) states[y] = Blocks.STONE.defaultBlockState();
|
||||
for (int y = 185; y <= 189; y++) states[y] = Blocks.WATER.defaultBlockState();
|
||||
for (int y = 210; y <= 260; y++) states[y] = Blocks.STONE.defaultBlockState();
|
||||
var column = new net.minecraft.world.level.NoiseColumn(0, states);
|
||||
helper.assertTrue(SwampHutAdmission25.localNativeHeight(column, 0, 193) == 190,
|
||||
"Native lower-hut height includes the local water surface");
|
||||
column.setBlock(190, Blocks.STONE.defaultBlockState());
|
||||
helper.assertTrue(SwampHutAdmission25.localNativeHeight(column, 0, 193) == 191,
|
||||
"Loaded native heightmap tags recognize a higher stone bank");
|
||||
helper.assertTrue(column.getBlock(210).is(Blocks.STONE),
|
||||
"Selecting the local waterline leaves the true cave roof in the certification column");
|
||||
}
|
||||
|
||||
private record Pier(BlockPos top, BlockPos foot, BlockState ground, BlockState subsoil) {}
|
||||
private record Witness(Terrain25Wetlands.Column column, BlockState floor, BlockState below) {}
|
||||
|
||||
private static final class Run {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<Witness> witnesses = new ArrayList<>();
|
||||
final List<Pier> piers = new ArrayList<>();
|
||||
final Set<ChunkPos> chunks = new LinkedHashSet<>(), entityChunks = new LinkedHashSet<>();
|
||||
final Map<Long, List<Terrain25Wetlands.Column>> strata = new HashMap<>();
|
||||
final long started = System.nanoTime();
|
||||
List<UndergroundPlan25.PlannedStart> huts = List.of();
|
||||
BiomeResolver withoutWetlands;
|
||||
Iterator<ChunkPos> iterator;
|
||||
boolean tickets;
|
||||
int wait;
|
||||
|
||||
Run(GameTestHelper helper) {
|
||||
this.helper = helper; level = helper.getLevel();
|
||||
generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
}
|
||||
|
||||
void begin() {
|
||||
try {
|
||||
helper.assertTrue(generator.rootGeneration() == 25, "Wetlands require a disposable generation25 world");
|
||||
boolean structures = level.structureManager().shouldGenerateStructures();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("generate_structures", structures); report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("underground_required", Boolean.getBoolean("sanctuary.test.wetlands25.undergroundRequired"));
|
||||
var binding = generator.starterContext();
|
||||
var capacity = PopulationHydrologyRuntime.capacity(binding.generator());
|
||||
var first = capacity.regionAt(-capacity.terrainLimit(), -capacity.terrainLimit());
|
||||
var last = capacity.regionAt(capacity.terrainLimit(), capacity.terrainLimit());
|
||||
var columns = new TreeMap<String, Terrain25Wetlands.Column>();
|
||||
var regions = new ArrayList<Map<String, Object>>();
|
||||
for (int rx = first.x(); rx <= last.x(); rx++) for (int rz = first.z(); rz <= last.z(); rz++) {
|
||||
var region = new IslandCapacity.Region(rx, rz);
|
||||
var plan = Terrain25Runtime.wetlandPlan(binding.generator(), binding.random(), region);
|
||||
helper.assertTrue(plan.candidates() <= 2 * Terrain25Wetlands.MAX_CANDIDATES
|
||||
&& plan.sites().stream().filter(s -> !s.underground()).count() <= Terrain25Wetlands.MAX_SITES
|
||||
&& plan.sites().stream().filter(Terrain25Wetlands.Site::underground).count() <= Terrain25Wetlands.MAX_UNDERGROUND_SITES,
|
||||
"Combined surface and underground wetland searches stay within their regional budgets");
|
||||
regions.add(Map.of("region", List.of(rx, rz), "sites", plan.sites(), "columns", plan.columns().size(),
|
||||
"candidates", plan.candidates(), "refusals", plan.refusals()));
|
||||
for (var c : plan.columns()) {
|
||||
// A biome lookup consults the owning region. Cross-border metadata alone is not a world biome.
|
||||
if (!capacity.regionAt(c.x(), c.z()).equals(region)) continue;
|
||||
helper.assertTrue((c.underground() || Math.hypot(c.x(), c.z()) >= Terrain25Wetlands.ARRIVAL_RADIUS)
|
||||
&& Math.hypot(c.x(), c.z()) < capacity.terrainLimit(), "Wetlands stay inside the island and preserve the surface arrival area");
|
||||
helper.assertTrue(c.minY() == c.floorY() - (c.wet() ? 2 : 3)
|
||||
&& c.maxY() <= Math.max(c.floorY(), c.waterY()) + 12
|
||||
&& c.maxY() >= Math.max(c.floorY(), c.waterY()) + 4, "Each wetland owns a finite local envelope, possibly limited by its cave ceiling");
|
||||
helper.assertTrue(c.underground() || c.maxY() == Math.max(c.floorY(), c.waterY()) + 12,
|
||||
"An exposed wetland retains its surface envelope");
|
||||
helper.assertTrue(plan.contains(c.x(), c.floorY(), c.z())
|
||||
&& !c.contains(c.minY() - 1) && !c.contains(c.maxY() + 1), "The individual wetland stratum has exact vertical bounds");
|
||||
if (c.wet()) helper.assertTrue(c.waterY() > c.floorY() && c.waterY() - c.floorY() <= (c.underground() ? 6 : 4),
|
||||
"Wetland water follows its existing shallow surface or underground retention");
|
||||
columns.put(c.x() + "," + c.z() + "," + c.floorY() + "," + c.waterY() + "," + c.waterFeature(), c);
|
||||
}
|
||||
}
|
||||
report.put("regions", regions); report.put("effective_columns", columns.size());
|
||||
var ordered = columns.values().stream().sorted(COLUMN_ORDER).toList();
|
||||
for (var c : ordered) strata.computeIfAbsent(columnKey(c.x(), c.z()), ignored -> new ArrayList<>()).add(c);
|
||||
long lowerColumns = ordered.stream().filter(Terrain25Wetlands.Column::underground).count();
|
||||
report.put("horizontal_columns", strata.size());
|
||||
report.put("surface_strata", ordered.size() - lowerColumns); report.put("underground_strata", lowerColumns);
|
||||
report.put("superposed_horizontal_columns", strata.values().stream().filter(list -> list.size() > 1).count());
|
||||
report.put("natural_plan_sha256", HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(JSON.toJson(ordered).getBytes(StandardCharsets.UTF_8))));
|
||||
if (Boolean.getBoolean("sanctuary.test.wetlands25.required"))
|
||||
helper.assertTrue(!columns.isEmpty(), "This fixture requires a positive naturally admitted wetland");
|
||||
if (Boolean.getBoolean("sanctuary.test.wetlands25.undergroundRequired"))
|
||||
helper.assertTrue(lowerColumns > 0, "This fixture requires a naturally admitted underground wetland");
|
||||
report.put("presence", !columns.isEmpty());
|
||||
report.put("presence_status", columns.isEmpty() ? "not_admitted_conditionally" : "admitted");
|
||||
var baseline = generator.source().copy(); baseline.setIslands(generator.source().islands());
|
||||
baseline.setTerrain25Wetlands(Terrain25Wetlands.Plan.EMPTY);
|
||||
withoutWetlands = baseline.createCachingResolver(binding.random());
|
||||
var underground = UndergroundStructures25.plan(generator);
|
||||
huts = underground.starts().stream().filter(p -> p.key().identifier().toString().equals("minecraft:swamp_hut")).toList();
|
||||
if (!structures || !generator.experimentalStructures())
|
||||
helper.assertTrue(huts.isEmpty(), "OFF disables the optional hut, independently of natural wetlands");
|
||||
report.put("hut_status", huts.isEmpty() ? "not_admitted_conditionally" : "admitted");
|
||||
report.put("hut_diagnostics", underground.diagnostics());
|
||||
for (boolean lower : new boolean[]{false, true}) for (boolean wet : new boolean[]{true, false}) {
|
||||
int added = 0;
|
||||
for (var c : ordered) {
|
||||
if (c.underground() != lower || c.wet() != wet || (c.x() & 3) != 0 || (c.z() & 3) != 0
|
||||
|| biomeY(c) > c.maxY()) continue;
|
||||
if (generator.intersectsExperimentalStructures(c.x(), c.floorY() - 2, c.z(), c.x(), c.maxY(), c.z())) continue;
|
||||
var column = binding.generator().getBaseColumn(c.x(), c.z(), level, binding.random());
|
||||
witnesses.add(new Witness(c, column.getBlock(c.floorY()), column.getBlock(c.floorY() - 1)));
|
||||
chunks.add(new ChunkPos(c.x() >> 4, c.z() >> 4));
|
||||
if (++added >= WITNESSES_PER_KIND) break;
|
||||
}
|
||||
if (ordered.stream().anyMatch(c -> c.underground() == lower && c.wet() == wet))
|
||||
helper.assertTrue(added > 0, "An admitted " + (lower ? "underground" : "surface")
|
||||
+ (wet ? " water" : " bank") + " stratum has a real unobstructed FULL witness");
|
||||
}
|
||||
if (!columns.isEmpty()) {
|
||||
helper.assertTrue(witnesses.stream().anyMatch(w -> w.column().wet()), "At least one actual wet quart is inspected");
|
||||
helper.assertTrue(witnesses.stream().anyMatch(w -> !w.column().wet()), "At least one actual shore quart is inspected");
|
||||
}
|
||||
for (var hut : huts) {
|
||||
helper.assertTrue(hut.start().getPieces().size() == 1 && hut.start().getPieces().getFirst() instanceof SwampHutPiece,
|
||||
"The admitted hut uses its actual vanilla piece");
|
||||
var piece = hut.start().getPieces().getFirst();
|
||||
chunks.add(hut.start().getChunkPos());
|
||||
piece.getBoundingBox().intersectingChunks().forEach(c -> { chunks.add(c); entityChunks.add(c); });
|
||||
for (var top : SwampHutAdmission25.piers(piece)) {
|
||||
var column = binding.generator().getBaseColumn(top.getX(), top.getZ(), level, binding.random());
|
||||
int footY = top.getY();
|
||||
while (replaceable(column.getBlock(footY)) && top.getY() - footY <= SwampHutAdmission25.MAX_PIER_DROP) footY--;
|
||||
helper.assertTrue(top.getY() - footY <= SwampHutAdmission25.MAX_PIER_DROP,
|
||||
"A planned native pier reaches natural terrain within eight blocks");
|
||||
var foot = new BlockPos(top.getX(), footY, top.getZ());
|
||||
var ground = column.getBlock(footY); var subsoil = column.getBlock(footY - 1);
|
||||
helper.assertTrue(solid(ground) && solid(subsoil), "Two natural ground layers anchor each hut pier before FULL");
|
||||
piers.add(new Pier(top, foot, ground, subsoil));
|
||||
}
|
||||
}
|
||||
chunks.add(new ChunkPos(0, 0));
|
||||
int voidX = (capacity.terrainLimit() + 255) & ~15;
|
||||
chunks.add(new ChunkPos(voidX >> 4, 0)); report.put("void_witness_x", voidX);
|
||||
helper.assertTrue(chunks.size() <= MAX_FULL_CHUNKS, "Wetland test loads only bounded native witnesses");
|
||||
report.put("full_chunks_requested", chunks.stream().map(c -> List.of(c.x(), c.z())).toList());
|
||||
iterator = chunks.iterator(); step();
|
||||
} catch (Throwable failure) { fail(failure); }
|
||||
}
|
||||
|
||||
void step() {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var c = iterator.next(); level.getChunk(c.x(), c.z(), ChunkStatus.FULL, true);
|
||||
later(); return;
|
||||
}
|
||||
if (!tickets) { entityChunks.forEach(c -> ChunkTickets25.add(level, c)); tickets = true; }
|
||||
boolean ready = entityChunks.stream().allMatch(c -> level.areEntitiesLoaded(c.pack()) && level.areEntitiesActuallyLoadedAndTicking(c));
|
||||
if (!ready && wait++ < 6000) { later(); return; }
|
||||
report.put("entity_wait_ticks", wait);
|
||||
report.put("entity_chunks", entityChunks.stream().map(c -> Map.of("chunk", List.of(c.x(), c.z()),
|
||||
"loaded", level.areEntitiesLoaded(c.pack()), "ticking", level.areEntitiesActuallyLoadedAndTicking(c),
|
||||
"status", level.getChunkSource().getChunkDebugData(c))).toList());
|
||||
helper.assertTrue(ready, "Hut occupants are inspected after all hut chunks are actually ticking");
|
||||
verifyWorld(); release(); report.put("passed", true);
|
||||
report.put("milliseconds", (System.nanoTime() - started) / 1_000_000L);
|
||||
String mode = !level.structureManager().shouldGenerateStructures() ? "global-off"
|
||||
: !generator.experimentalStructures() ? "experimental-off" : "create";
|
||||
write(Path.of("diagnostics/wetlands25/" + mode + ".json"), report); helper.succeed();
|
||||
} catch (Throwable failure) { fail(failure); }
|
||||
}
|
||||
|
||||
void verifyWorld() {
|
||||
var results = new ArrayList<Map<String, Object>>(); report.put("native_witnesses", results);
|
||||
int wet = 0, bank = 0, soils = 0, caveQuarts = 0;
|
||||
var checkedColumns = new HashSet<Long>();
|
||||
var stratumCounts = new LinkedHashMap<String, Integer>();
|
||||
var soilCounts = new LinkedHashMap<String, Integer>();
|
||||
var ores = new LinkedHashMap<BlockPos, Map<String, Object>>();
|
||||
report.put("ore_witnesses", new ArrayList<Map<String, Object>>());
|
||||
var binding = generator.starterContext();
|
||||
for (var witness : witnesses) {
|
||||
var c = witness.column(); int biomeY = biomeY(c);
|
||||
String stratum = (c.underground() ? "underground_" : "surface_") + (c.wet() ? "water" : "bank");
|
||||
stratumCounts.merge(stratum, 1, Integer::sum);
|
||||
var floor = new BlockPos(c.x(), c.floorY(), c.z());
|
||||
var actual = level.getBlockState(floor); var below = level.getBlockState(floor.below());
|
||||
var result = new LinkedHashMap<String, Object>(); results.add(result);
|
||||
result.put("column", c); result.put("floor_before_full", block(witness.floor()));
|
||||
result.put("stratum", stratum);
|
||||
result.put("floor_full", block(actual)); result.put("subsoil_full", block(below));
|
||||
result.put("biome_y", biomeY);
|
||||
var biome = level.getChunk(c.x() >> 4, c.z() >> 4).getNoiseBiome(c.x() >> 2, biomeY >> 2, c.z() >> 2);
|
||||
result.put("stored_biome", biome.unwrapKey().orElseThrow().identifier().toString());
|
||||
helper.assertTrue(biome.is(Terrain25Biomes.key("swamp")), "Actual FULL stores swamp at its water/shore envelope: " + floor);
|
||||
helper.assertTrue(solid(actual) && solid(below) && solid(level.getBlockState(floor.below(2))),
|
||||
"Actual wetland floor and two underlying layers remain supported: " + floor);
|
||||
if (c.wet()) {
|
||||
for (int y = c.floorY() + 1; y <= c.waterY(); y++) {
|
||||
var pos = new BlockPos(c.x(), y, c.z()); var fluid = level.getFluidState(pos);
|
||||
helper.assertTrue(fluid.is(FluidTags.WATER) && fluid.isSource(), "Shallow retained water survives FULL without a synthetic refill: " + pos);
|
||||
}
|
||||
wet++;
|
||||
} else bank++;
|
||||
var expected = Terrain25Runtime.shoreMaterial(c, level.getSeed(), c.floorY(), Blocks.STONE.defaultBlockState());
|
||||
boolean coatedBefore = witness.floor().equals(expected);
|
||||
boolean tree = actual.is(BlockTags.LOGS) || level.getBlockState(floor.above()).is(BlockTags.LOGS);
|
||||
boolean floorOre = nativeOre(actual), belowOre = nativeOre(below);
|
||||
result.put("expected_coating", block(expected)); result.put("tree_modification", tree);
|
||||
result.put("floor_is_preserved_ore", floorOre); result.put("subsoil_is_preserved_ore", belowOre);
|
||||
for (int depth = 0; depth <= 1; depth++) {
|
||||
var pos = floor.below(depth); var state = depth == 0 ? actual : below;
|
||||
var previous = depth == 0 ? witness.floor() : witness.below();
|
||||
if (nativeOre(previous)) helper.assertTrue(state.equals(previous),
|
||||
"An ore already present in the native preview survives FULL unchanged: " + pos);
|
||||
if (!nativeOre(state)) continue;
|
||||
helper.assertTrue(Terrain25Runtime.shoreMaterial(c, level.getSeed(), pos.getY(), state).equals(state),
|
||||
"Wetland coating preserves this real native ore instead of replacing it with soil: " + pos);
|
||||
ores.putIfAbsent(pos, Map.of("position", position(pos), "block", block(state), "stratum", stratum,
|
||||
"unchanged_from_preview", state.equals(previous), "coating_preserves_ore", true));
|
||||
}
|
||||
if (coatedBefore && !tree) {
|
||||
// Only actual ore is exempt: a remaining TUFF/STONE block must still fail.
|
||||
if (!floorOre) helper.assertTrue(actual.equals(expected), "Native wetland coating matches its deterministic soil at " + floor);
|
||||
if (!belowOre) helper.assertTrue(below.equals(witness.below()), "Native wetland subsoil survives decoration at " + floor);
|
||||
if (!floorOre && !belowOre) { soils++; soilCounts.merge(stratum, 1, Integer::sum); }
|
||||
}
|
||||
int outside = 0, caves = 0;
|
||||
boolean firstAtColumn = checkedColumns.add(columnKey(c.x(), c.z()));
|
||||
int inside = 0;
|
||||
for (int y = 0; firstAtColumn && y < 384; y += 4) {
|
||||
int quartY = y;
|
||||
boolean planned = strata.getOrDefault(columnKey(c.x(), c.z()), List.of()).stream().anyMatch(s -> s.contains(quartY));
|
||||
boolean runtime = Terrain25Runtime.wetlandAt(binding.generator(), binding.random(), c.x(), y, c.z());
|
||||
helper.assertTrue(runtime == planned, "World lookup preserves the union of every local wetland stratum at " + new BlockPos(c.x(), y, c.z()));
|
||||
var stored = level.getChunk(c.x() >> 4, c.z() >> 4).getNoiseBiome(c.x() >> 2, y >> 2, c.z() >> 2);
|
||||
if (planned) {
|
||||
helper.assertTrue(stored.is(Terrain25Biomes.key("swamp")), "Every claimed stratum stores the native swamp biome at " + new BlockPos(c.x(), y, c.z()));
|
||||
inside++; continue;
|
||||
}
|
||||
var baseline = withoutWetlands.getNoiseBiome(c.x() >> 2, y >> 2, c.z() >> 2);
|
||||
helper.assertTrue(stored.is(baseline.unwrapKey().orElseThrow()), "Outside all local wetland envelopes, native cave/land biome is unchanged at " + new BlockPos(c.x(), y, c.z()));
|
||||
helper.assertTrue(!stored.is(Terrain25Biomes.key("swamp")), "No vertical swamp leak outside the union of its envelopes");
|
||||
if (stored.is(Terrain25Biomes.key("lush_caves")) || stored.is(Terrain25Biomes.key("dripstone_caves"))
|
||||
|| stored.is(Terrain25Biomes.key("sulfur_depths")) || stored.is(Terrain25Biomes.MUSHROOM_REFUGE)) caves++;
|
||||
outside++;
|
||||
}
|
||||
result.put("all_strata_in_horizontal_column", strata.getOrDefault(columnKey(c.x(), c.z()), List.of()));
|
||||
result.put("vertical_column_checked_once", firstAtColumn); result.put("inside_envelopes_quarts_checked", inside);
|
||||
result.put("outside_envelope_quarts_checked", outside); result.put("cave_quarts_preserved", caves); caveQuarts += caves;
|
||||
}
|
||||
if (!witnesses.isEmpty()) helper.assertTrue(soils > 0, "At least one deterministic wetland soil is present in actual FULL terrain");
|
||||
for (String stratum : stratumCounts.keySet())
|
||||
helper.assertTrue(soilCounts.getOrDefault(stratum, 0) > 0, "Actual wetland coating is inspected separately in " + stratum);
|
||||
if (Boolean.getBoolean("sanctuary.test.wetlands25.undergroundRequired"))
|
||||
helper.assertTrue(stratumCounts.getOrDefault("underground_water", 0) > 0
|
||||
&& stratumCounts.getOrDefault("underground_bank", 0) > 0, "Required underground wetland has actual FULL water and bank witnesses");
|
||||
report.put("strata_checked", stratumCounts); report.put("strata_exact_soils_checked", soilCounts);
|
||||
report.put("vertical_columns_checked", checkedColumns.size());
|
||||
report.put("water_columns_checked", wet); report.put("shore_columns_checked", bank);
|
||||
report.put("exact_soil_columns_checked", soils); report.put("cave_quarts_preserved", caveQuarts);
|
||||
report.put("ore_witnesses", new ArrayList<>(ores.values())); report.put("ore_blocks_present", ores.size());
|
||||
report.put("ore_blocks_unchanged_from_preview", ores.values().stream().filter(o -> Boolean.TRUE.equals(o.get("unchanged_from_preview"))).count());
|
||||
report.put("ore_scope", "Actual ore is counted separately from exact soil pairs. Where preview already contains ore, FULL must preserve its exact state; later native ore remains excluded from coating. No ore presence quota or whole-deposit count.");
|
||||
int lowerArrivalQuarts = 0;
|
||||
for (int x : new int[]{0, (Integer) report.get("void_witness_x")}) for (int y = 0; y < 384; y += 4) {
|
||||
int quartY = y;
|
||||
boolean lowerAtArrival = x == 0 && strata.getOrDefault(columnKey(0, 0), List.of()).stream()
|
||||
.anyMatch(c -> c.underground() && c.contains(quartY));
|
||||
helper.assertTrue(Terrain25Runtime.wetlandAt(binding.generator(), binding.random(), x, y, 0) == lowerAtArrival,
|
||||
"Arrival stays protected at the surface; only a real underground envelope can be swamp below it, and none in the void");
|
||||
helper.assertTrue(level.getChunk(x >> 4, 0).getNoiseBiome(x >> 2, y >> 2, 0).is(Terrain25Biomes.key("swamp")) == lowerAtArrival,
|
||||
"Actual FULL respects the arrival surface and void, without excluding legitimate underground strata");
|
||||
if (lowerAtArrival) lowerArrivalQuarts++;
|
||||
}
|
||||
report.put("underground_quarts_below_arrival", lowerArrivalQuarts);
|
||||
verifyFlora();
|
||||
verifyHuts();
|
||||
report.put("scope", "Stored FULL biome quarts, real retained water and soils in separately selected upper/lower strata; all Y quarts outside the UNION of local envelopes match the baseline. OFF preserves natural planning. Hut presence is conditional. No global swamp/hut quota or lifecycle claim.");
|
||||
}
|
||||
|
||||
void verifyFlora() {
|
||||
var counts = new LinkedHashMap<String, Map<String, Integer>>(); report.put("native_flora", counts);
|
||||
var examples = new ArrayList<Map<String, Object>>(); report.put("native_flora_witnesses", examples);
|
||||
var scanned = new LinkedHashMap<String, Integer>(); report.put("flora_columns_scanned", scanned);
|
||||
var seen = new HashSet<BlockPos>();
|
||||
int lowerPlants = 0, plantsUnderRock = 0;
|
||||
var ordered = strata.values().stream().flatMap(Collection::stream).sorted(COLUMN_ORDER).toList();
|
||||
for (boolean lower : new boolean[]{false, true}) {
|
||||
String key = lower ? "underground" : "surface"; int columns = 0, exampleCount = 0;
|
||||
var plants = new LinkedHashMap<String, Integer>(); counts.put(key, plants);
|
||||
for (var c : ordered) {
|
||||
if (c.underground() != lower || !chunks.contains(new ChunkPos(c.x() >> 4, c.z() >> 4))) continue;
|
||||
// Keep native support checks inside chunks already explicitly loaded by this test.
|
||||
if ((c.x() & 15) == 0 || (c.x() & 15) == 15 || (c.z() & 15) == 0 || (c.z() & 15) == 15) continue;
|
||||
if (columns++ >= MAX_FLORA_COLUMNS_PER_STRATUM) { columns--; break; }
|
||||
for (int y = c.floorY() + 1; y <= c.maxY(); y++) {
|
||||
var pos = new BlockPos(c.x(), y, c.z()); var state = level.getBlockState(pos);
|
||||
if (!marshPlant(state) || !seen.add(pos)) continue;
|
||||
helper.assertTrue(state.canSurvive(level, pos), "Native marsh plant has actual valid support: " + pos + " " + block(state));
|
||||
plants.merge(block(state), 1, Integer::sum); if (lower) lowerPlants++;
|
||||
Integer roof = null;
|
||||
if (lower) for (int roofY = c.maxY() + 1; roofY <= Math.min(383, c.maxY() + 64); roofY++) {
|
||||
var top = level.getBlockState(new BlockPos(c.x(), roofY, c.z()));
|
||||
if (solid(top) && !top.is(BlockTags.LOGS) && !top.is(BlockTags.LEAVES)) { roof = roofY; break; }
|
||||
}
|
||||
if (roof != null) plantsUnderRock++;
|
||||
if (exampleCount++ < 24) {
|
||||
var example = new LinkedHashMap<String, Object>(); examples.add(example);
|
||||
example.put("position", position(pos)); example.put("block", block(state)); example.put("stratum", key);
|
||||
example.put("rock_roof_y", roof); example.put("native_support_valid", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
scanned.put(key, columns);
|
||||
}
|
||||
report.put("underground_flora_blocks", lowerPlants); report.put("underground_flora_under_rock", plantsUnderRock);
|
||||
if (Boolean.getBoolean("sanctuary.test.wetlands25.undergroundRequired"))
|
||||
helper.assertTrue(lowerPlants > 0 && plantsUnderRock > 0,
|
||||
"Required underground wetland has real supported marsh vegetation beneath an actual rock roof");
|
||||
}
|
||||
|
||||
void verifyHuts() {
|
||||
var results = new ArrayList<Map<String, Object>>(); report.put("native_huts", results);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
for (var planned : huts) {
|
||||
var at = planned.start().getChunkPos();
|
||||
var start = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getStartForStructure(planned.start().getStructure());
|
||||
helper.assertTrue(start != null && start.isValid() && start.getPieces().size() == 1
|
||||
&& start.getPieces().getFirst() instanceof SwampHutPiece, "Admitted hut has its actual saved native start");
|
||||
var piece = start.getPieces().getFirst(); var tag = piece.createTag(context);
|
||||
helper.assertTrue(tag.getBooleanOr("Witch", false) && tag.getBooleanOr("Cat", false), "Native one-time occupant flags are saved");
|
||||
var saved = start.createTag(context, at); var decoded = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(decoded != null && saved.equals(decoded.createTag(context, at)), "Native hut state round-trips through its real NBT");
|
||||
var b = piece.getBoundingBox(); var search = new AABB(b.minX() - 16, b.minY() - 16, b.minZ() - 16, b.maxX() + 17, b.maxY() + 17, b.maxZ() + 17);
|
||||
var witches = level.getEntitiesOfClass(Witch.class, search, e -> e.isAlive() && e.isPersistenceRequired());
|
||||
var cats = level.getEntitiesOfClass(Cat.class, search, e -> e.isAlive() && e.isPersistenceRequired());
|
||||
helper.assertTrue(!witches.isEmpty() && !cats.isEmpty(), "A real persistent witch and cat inhabit the admitted hut");
|
||||
for (var pier : piers) {
|
||||
helper.assertTrue(level.getBlockState(pier.foot()).equals(pier.ground())
|
||||
&& level.getBlockState(pier.foot().below()).equals(pier.subsoil()), "Native hut preserves its original two-layer natural footing");
|
||||
for (int y = pier.foot().getY() + 1; y <= pier.top().getY(); y++)
|
||||
helper.assertTrue(level.getBlockState(new BlockPos(pier.top().getX(), y, pier.top().getZ())).is(Blocks.OAK_LOG),
|
||||
"Every native pier is continuous down to its actual natural footing");
|
||||
}
|
||||
results.add(Map.of("start_chunk", List.of(at.x(), at.z()), "entry", position(planned.entrance()),
|
||||
"witches", witches.stream().map(e -> e.getUUID().toString()).toList(), "cats", cats.stream().map(e -> e.getUUID().toString()).toList(),
|
||||
"piers", piers.stream().map(p -> Map.of("top", position(p.top()), "foot", position(p.foot()), "natural_ground", block(p.ground()))).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
void later() { helper.runAtTickTime(helper.getTick() + 1, this::step); }
|
||||
void release() { if (tickets) entityChunks.forEach(c -> ChunkTickets25.remove(level, c)); tickets = false; }
|
||||
void fail(Throwable failure) {
|
||||
release(); report.put("passed", false); report.put("failure", failure.toString());
|
||||
var stack = new StringWriter(); failure.printStackTrace(new PrintWriter(stack)); report.put("stacktrace", stack.toString());
|
||||
try { write(Path.of("diagnostics/wetlands25/failure.json"), report); } catch (Exception ignored) {}
|
||||
throw new IllegalStateException("Native wetlands25 verification failed", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
|
||||
private static boolean nativeOre(BlockState state) {
|
||||
return state.is(BlockTags.ORES);
|
||||
}
|
||||
private static boolean marshPlant(BlockState state) {
|
||||
return state.is(Blocks.LILY_PAD) || state.is(Blocks.SUGAR_CANE) || state.is(Blocks.SHORT_GRASS)
|
||||
|| state.is(Blocks.TALL_GRASS) || state.is(Blocks.BLUE_ORCHID) || state.is(Blocks.FIREFLY_BUSH)
|
||||
|| state.is(Blocks.BROWN_MUSHROOM) || state.is(Blocks.RED_MUSHROOM) || state.is(Blocks.VINE);
|
||||
}
|
||||
private static int biomeY(Terrain25Wetlands.Column column) { return (column.floorY() + 4) & ~3; }
|
||||
private static long columnKey(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); }
|
||||
private static boolean replaceable(BlockState state) { return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN)
|
||||
|| state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS); }
|
||||
private static String block(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); }
|
||||
private static List<Integer> position(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static void write(Path path, Object value) throws java.io.IOException {
|
||||
Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(value) + "\n");
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -27,6 +27,8 @@ public final class WorldgenPerformanceGameTests {
|
||||
var capacity = IslandCapacity.forGenerator(binding.generator());
|
||||
var spawn = SanctuaryWorldGameTests.initialSpawn.pos();
|
||||
int cx = spawn.getX() >> 4, cz = spawn.getZ() >> 4;
|
||||
boolean fixed = Boolean.getBoolean("sanctuary.test.benchmark.fixedCoordinates");
|
||||
if (fixed) { cx = 0; cz = 0; }
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
|
||||
.getMetadata().getVersion().getFriendlyString());
|
||||
@@ -34,13 +36,25 @@ public final class WorldgenPerformanceGameTests {
|
||||
var host = level.getChunkSource().getGenerator();
|
||||
String profile = "players-" + capacity.players();
|
||||
if (host instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator unified) {
|
||||
report.put("generation", 13); report.put("initial_diameter", unified.initialDiameter());
|
||||
report.put("generation", unified.rootGeneration()); report.put("initial_diameter", unified.initialDiameter());
|
||||
profile = "unified-" + unified.initialDiameter();
|
||||
}
|
||||
report.put("spawn", List.of(spawn.getX(), spawn.getY(), spawn.getZ()));
|
||||
report.put("java", System.getProperty("java.version"));
|
||||
report.put("processors", Runtime.getRuntime().availableProcessors());
|
||||
report.put("max_heap_bytes", Runtime.getRuntime().maxMemory());
|
||||
report.put("fixed_coordinates", fixed);
|
||||
report.put("chunk_origin", List.of(cx, cz));
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("heap_before_bytes", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
|
||||
report.put("fluid_scheduler_before", FluidTiming25.snapshot());
|
||||
var alreadyLoaded = new ArrayList<List<Integer>>();
|
||||
for (int x = -8; x <= 24; x++) for (int z = -8; z <= 8; z++) {
|
||||
if (x > 8 && Math.abs(z) > 2) continue;
|
||||
if (level.getChunkSource().getChunkNow(cx + x, cz + z) != null)
|
||||
alreadyLoaded.add(List.of(cx + x, cz + z));
|
||||
}
|
||||
report.put("workload_full_chunks_already_loaded", alreadyLoaded);
|
||||
List<Map<String, Object>> rings = new ArrayList<>();
|
||||
List<Long> chunkTimes = new ArrayList<>();
|
||||
int count = 0, land = 0;
|
||||
@@ -86,7 +100,13 @@ public final class WorldgenPerformanceGameTests {
|
||||
report.put("exploration_full_chunks", 80); report.put("exploration_steps", steps);
|
||||
report.put("exploration_chunk_latency_ms", latency(chunkTimes));
|
||||
report.put("regions_cached", PopulationHydrologyRuntime.cachedRegionCount(binding.random()));
|
||||
report.put("heap_after_bytes", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
|
||||
report.put("memory_pools", java.lang.management.ManagementFactory.getMemoryPoolMXBeans().stream()
|
||||
.map(pool -> Map.of("name", pool.getName(), "type", pool.getType().toString(),
|
||||
"used_bytes", pool.getUsage().getUsed(), "peak_used_bytes", pool.getPeakUsage().getUsed())).toList());
|
||||
report.put("fluid_scheduler_after", FluidTiming25.snapshot());
|
||||
report.put("scope", "Fresh development world; 289 FULL chunks around natural spawn, then 80 eastward FULL chunks. "
|
||||
+ (fixed ? "Fixed-coordinate mode uses chunk0,0 instead of spawn for equal cross-version footprints. " : "")
|
||||
+ "Synchronous server requests include dependent generation up to FULL, not guaranteed ticking readiness, client GPU, network, FPS or player load. "
|
||||
+ "World creation before the test is timed separately by Minecraft's startup log.");
|
||||
helper.assertTrue(count == 289 && land > 100, "The benchmark must generate actual playable terrain");
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import fr.koka.sanctuary.gametest.FluidTiming25;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.ticks.LevelTicks;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
@Mixin(ServerLevel.class)
|
||||
public abstract class FluidTiming25Mixin {
|
||||
@WrapOperation(method = "tick", at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/world/ticks/LevelTicks;tick(JILjava/util/function/BiConsumer;)V"))
|
||||
private <T> void sanctuary$measureFluidScheduler(LevelTicks<T> ticks, long time, int limit,
|
||||
BiConsumer<BlockPos, T> consumer, Operation<Void> original) {
|
||||
var level = (ServerLevel) (Object) this;
|
||||
if (ticks != level.getFluidTicks() || !level.dimension().equals(Level.OVERWORLD)) {
|
||||
original.call(ticks, time, limit, consumer); return;
|
||||
}
|
||||
long[] count = {0};
|
||||
BiConsumer<BlockPos, T> measured = (pos, fluid) -> { count[0]++; consumer.accept(pos, fluid); };
|
||||
long start = System.nanoTime();
|
||||
try { original.call(ticks, time, limit, measured); }
|
||||
finally { FluidTiming25.record(System.nanoTime() - start, count[0], ticks.count()); }
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||
if (Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|
||||
if (Boolean.getBoolean("sanctuary.test.benchmark.fixedCoordinates") || Integer.getInteger("sanctuary.test.generation", 25) == 25 || Boolean.getBoolean("sanctuary.test.monument25") || Boolean.getBoolean("sanctuary.test.terrain25") || Boolean.getBoolean("sanctuary.test.underground25") || Boolean.getBoolean("sanctuary.test.hydrology25") || Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|
||||
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||
long until = System.nanoTime() + 20_000_000L;
|
||||
@@ -47,11 +47,11 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 24);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 24 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 25);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24 && generation != 25) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 25 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
if (Boolean.getBoolean("sanctuary.test.terrain25") || Boolean.getBoolean("sanctuary.test.underground25") || Boolean.getBoolean("sanctuary.test.monument25") || Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
String preset = switch (players) {
|
||||
case 5 -> "sanctuary_5";
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"required": true,
|
||||
"package": "fr.koka.sanctuary.gametest.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": ["SanctuaryGameTestServerMixin"],
|
||||
"injectors": {"defaultRequire": 1}
|
||||
"mixins": [
|
||||
"SanctuaryGameTestServerMixin",
|
||||
"FluidTiming25Mixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,12 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v25"), SanctuaryChunkGenerator.TERRAIN25_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v25"), SanctuaryBiomeSource.TERRAIN25_CODEC);
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("terrain25"), fr.koka.sanctuary.worldgen.Terrain25Density.CODEC);
|
||||
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.register();
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("terrain25_groves"), fr.koka.sanctuary.worldgen.Terrain25GrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("terrain25_refuge"), fr.koka.sanctuary.worldgen.Terrain25RefugeFeature.CODEC);
|
||||
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
|
||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||
|
||||
+2
-2
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
contents.addChild(sizeButton);
|
||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
|
||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
|
||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
||||
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||
(button, value) -> experimental = value));
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 25 ? "sanctuary.world_options.terrain25_hint" : generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
}
|
||||
layout.addToContents(contents);
|
||||
|
||||
+7
-1
@@ -130,6 +130,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
@@ -146,7 +147,12 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
@Override public void applyBiomeDecoration(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
var island = islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (!ExpansionOcean23.active(island)) { super.applyBiomeDecoration(level, chunk, structures); return; }
|
||||
if (!ExpansionOcean23.active(island)) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
if (rootGeneration() == 25 && island != null && island.origin())
|
||||
fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.decorate(level, chunk);
|
||||
return;
|
||||
}
|
||||
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
|
||||
try { super.applyBiomeDecoration(level, chunk, structures); }
|
||||
finally { ExpansionOcean23.restoreDecoration(previous); }
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
/** Initial river outlets reserve their future falling water before exploration; no chunks are requested. */
|
||||
public final class ExpansionInitialFlows25 {
|
||||
public static final int SPREAD = 7;
|
||||
private static final Map<SanctuaryChunkGenerator, Set<Long>> CLAIMS = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private ExpansionInitialFlows25() {}
|
||||
|
||||
public static Set<Long> prepare(SanctuaryChunkGenerator generator) {
|
||||
if (generator.rootGeneration() != 25) return Set.of();
|
||||
synchronized (CLAIMS) {
|
||||
var existing = CLAIMS.get(generator);
|
||||
if (existing != null) return existing;
|
||||
var binding = generator.starterContext();
|
||||
var capacity = PopulationHydrologyRuntime.capacity(binding.generator());
|
||||
int limit = capacity.terrainLimit();
|
||||
var first = capacity.regionAt(-limit, -limit); var last = capacity.regionAt(limit, limit);
|
||||
var falls = new ArrayList<Terrain25Runtime.Fall>();
|
||||
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
|
||||
var region = new IslandCapacity.Region(x, z);
|
||||
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
|
||||
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nearestX, nearestZ) < limit)
|
||||
falls.addAll(Terrain25Runtime.falls(binding.generator(), binding.random(), region));
|
||||
}
|
||||
var result = fromFalls(falls);
|
||||
CLAIMS.put(generator, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** A column from Y0 to the recorded waterline occupies its chunk for expansion admission.
|
||||
* Only actual channel cells receive the local seven-block fluid margin, never the full diagonal bounding box. */
|
||||
public static Set<Long> fromFalls(List<Terrain25Runtime.Fall> falls) {
|
||||
var chunks = new HashSet<Long>();
|
||||
for (var fall : falls) {
|
||||
if (fall.waterY() < 1 || fall.waterY() > 383 || fall.width() < 1 || fall.width() > 32
|
||||
|| Math.abs((long) fall.endX() - fall.startX()) > 64 || Math.abs((long) fall.endZ() - fall.startZ()) > 64
|
||||
|| fall.startX() == fall.endX() && fall.startZ() == fall.endZ())
|
||||
throw new IllegalArgumentException("Invalid initial waterfall reservation");
|
||||
for (int x = fall.minX(); x <= fall.maxX(); x++) for (int z = fall.minZ(); z <= fall.maxZ(); z++) {
|
||||
if (!fall.channel(x, z)) continue;
|
||||
for (int cx = (x - SPREAD) >> 4; cx <= (x + SPREAD) >> 4; cx++)
|
||||
for (int cz = (z - SPREAD) >> 4; cz <= (z + SPREAD) >> 4; cz++) chunks.add(new ChunkPos(cx, cz).pack());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(chunks);
|
||||
}
|
||||
|
||||
public static boolean conflicts(SanctuaryChunkGenerator generator, ExpansionIsland island) {
|
||||
return generator.rootGeneration() == 25 && conflicts(prepare(generator), island);
|
||||
}
|
||||
public static boolean conflicts(Set<Long> claims, ExpansionIsland island) {
|
||||
if (island.origin()) return false;
|
||||
for (long key : claims) {
|
||||
var chunk = ChunkPos.unpack(key);
|
||||
if (island.ownsChunk(chunk.x(), chunk.z())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import java.util.List;
|
||||
/** Immutable alpha.12 region contract. Directions place islands; climate remains an explicit choice. */
|
||||
public record ExpansionIsland(String id, String parent, String direction, String climate,
|
||||
int diameter, int centerX, int centerZ, long seed, String relief, String status, int generation, String reuseToken,
|
||||
List<String> structurePriorities) {
|
||||
List<String> structurePriorities, java.util.Optional<ExpansionStructurePlan25> structurePlan25) {
|
||||
public static final int GENERATION = 12;
|
||||
public static final List<String> DIRECTIONS = List.of("north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest");
|
||||
public static final List<String> CLIMATES = List.of("temperate", "boreal", "glacial", "cold_humid", "humid", "tropical", "warm", "arid", "dry", "cold_dry", "autumn", "cherry", "lost_city");
|
||||
@@ -30,7 +30,8 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
Codec.STRING.fieldOf("status").forGetter(ExpansionIsland::status),
|
||||
Codec.INT.fieldOf("generation").forGetter(ExpansionIsland::generation),
|
||||
Codec.STRING.optionalFieldOf("reuse_token", "").forGetter(ExpansionIsland::reuseToken),
|
||||
Codec.STRING.listOf().optionalFieldOf("structure_priorities", List.of()).forGetter(ExpansionIsland::structurePriorities)
|
||||
Codec.STRING.listOf().optionalFieldOf("structure_priorities", List.of()).forGetter(ExpansionIsland::structurePriorities),
|
||||
ExpansionStructurePlan25.CODEC.optionalFieldOf("structure_plan25").forGetter(ExpansionIsland::structurePlan25)
|
||||
).apply(i, ExpansionIsland::new));
|
||||
|
||||
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
|
||||
@@ -42,10 +43,15 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, List.of());
|
||||
}
|
||||
|
||||
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
|
||||
int centerX, int centerZ, long seed, String relief, String status, int generation, String reuseToken, List<String> structurePriorities) {
|
||||
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities, java.util.Optional.empty());
|
||||
}
|
||||
|
||||
public ExpansionIsland {
|
||||
if (!id.matches("[a-z][a-z0-9_]{0,31}") || !parent.matches("[a-z][a-z0-9_]{0,31}")) throw new IllegalArgumentException("Invalid island identifier");
|
||||
if (!DIRECTIONS.contains(direction) || !CLIMATES.contains(climate) || !reliefs(generation).contains(relief)) throw new IllegalArgumentException("Unknown island profile");
|
||||
if (relief.equals("ocean") && ((generation != 23 && generation != 24) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
|
||||
if (relief.equals("ocean") && ((generation != 23 && generation != 24 && generation != 25) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
|
||||
throw new IllegalArgumentException("Ocean relief requires a generation23 expansion of diameter512 or1024");
|
||||
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
|
||||
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
|
||||
@@ -59,8 +65,12 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
var priorities = new java.util.TreeSet<>(structurePriorities);
|
||||
if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family");
|
||||
structurePriorities = List.copyOf(priorities);
|
||||
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24) || id.equals("sanctuary")))
|
||||
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24 && generation != 25) || id.equals("sanctuary")))
|
||||
throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions");
|
||||
if (structurePlan25 == null || structurePlan25.isPresent() && (generation != 25 || id.equals("sanctuary") || !relief.equals("ocean")))
|
||||
throw new IllegalArgumentException("Saved native structure plans require an ocean25 expansion");
|
||||
if (structurePlan25.isPresent()) structurePlan25.get().validateFor(new ExpansionIsland(id, parent, direction, climate,
|
||||
diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities));
|
||||
}
|
||||
|
||||
public static ExpansionIsland origin(int diameter, long seed) {
|
||||
@@ -70,8 +80,8 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
|
||||
}
|
||||
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24); }
|
||||
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24) ? RELIEFS23 : RELIEFS; }
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25); }
|
||||
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24 || generation == 25) ? RELIEFS23 : RELIEFS; }
|
||||
public boolean origin() { return id.equals("sanctuary"); }
|
||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||
@@ -89,10 +99,13 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
return Math.abs((long) centerX - other.centerX) <= reservedRadius() + other.reservedRadius()
|
||||
&& Math.abs((long) centerZ - other.centerZ) <= reservedRadius() + other.reservedRadius();
|
||||
}
|
||||
public ExpansionIsland ready() { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, "ready", generation, reuseToken, structurePriorities); }
|
||||
public ExpansionIsland withReuseToken(String token) { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, token, structurePriorities); }
|
||||
public ExpansionIsland ready() { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, "ready", generation, reuseToken, structurePriorities, structurePlan25); }
|
||||
public ExpansionIsland withReuseToken(String token) { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, token, structurePriorities, structurePlan25); }
|
||||
public ExpansionIsland withStructurePriorities(List<String> priorities) {
|
||||
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, priorities);
|
||||
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, priorities, structurePlan25);
|
||||
}
|
||||
public ExpansionIsland withStructurePlan25(ExpansionStructurePlan25 plan) {
|
||||
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, structurePriorities, java.util.Optional.of(plan));
|
||||
}
|
||||
public static String directionalClimate(String direction) {
|
||||
return switch (direction) {
|
||||
|
||||
@@ -76,13 +76,13 @@ public final class ExpansionJournal {
|
||||
this.file = this.directory.resolve(FILE_NAME);
|
||||
this.seed = seed;
|
||||
this.rootDiameter = rootDiameter;
|
||||
if (rootGeneration == 24 && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
|
||||
else if (rootGeneration != 24 && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
|
||||
if ((rootGeneration == 24 || rootGeneration == 25) && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
|
||||
else if ((rootGeneration != 24 && rootGeneration != 25) && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
|
||||
if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory");
|
||||
this.islands = read();
|
||||
} else {
|
||||
if (rootGeneration == 24 && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
|
||||
if ((rootGeneration == 24 || rootGeneration == 25) && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
|
||||
// Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair.
|
||||
Files.createDirectories(this.directory.getParent());
|
||||
Files.createDirectory(this.directory);
|
||||
@@ -164,31 +164,33 @@ public final class ExpansionJournal {
|
||||
|
||||
private List<ExpansionIsland> read() throws IOException {
|
||||
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Existing expansion directory has no regular journal file");
|
||||
if (Files.size(file) > MAX_BYTES) throw new IOException("Expansion journal exceeds its size limit");
|
||||
if (Files.size(file) > (rootGeneration == 25 ? 8_388_608 : MAX_BYTES)) throw new IOException("Expansion journal exceeds its size limit");
|
||||
try (JsonReader reader = new JsonReader(new StringReader(Files.readString(file, StandardCharsets.UTF_8)))) {
|
||||
reader.setStrictness(Strictness.STRICT);
|
||||
JsonObject root = readJson(reader, 0).getAsJsonObject();
|
||||
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal");
|
||||
var journalFields = new java.util.HashSet<>(JOURNAL_FIELDS);
|
||||
if (rootGeneration == 24) journalFields.add("aerial_plan");
|
||||
if ((rootGeneration == 24 || rootGeneration == 25)) journalFields.add("aerial_plan");
|
||||
requireFields(root, journalFields);
|
||||
if (integer(root, "schema") != (rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
|
||||
if (integer(root, "schema") != (rootGeneration == 25 ? 3 : rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
|
||||
throw new IOException("Expansion journal schema, world seed or root diameter does not match");
|
||||
}
|
||||
if (rootGeneration == 24) bindAerial(string(root, "aerial_plan"));
|
||||
if ((rootGeneration == 24 || rootGeneration == 25)) bindAerial(string(root, "aerial_plan"));
|
||||
JsonArray entries = root.getAsJsonArray("islands");
|
||||
List<ExpansionIsland> decoded = new ArrayList<>();
|
||||
for (JsonElement element : entries) {
|
||||
JsonObject entry = element.getAsJsonObject();
|
||||
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
|
||||
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
|
||||
if ((rootGeneration == 23 || rootGeneration == 24) && entry.has("structure_priorities")) fields.add("structure_priorities");
|
||||
if ((rootGeneration == 23 || (rootGeneration == 24 || rootGeneration == 25)) && entry.has("structure_priorities")) fields.add("structure_priorities");
|
||||
if (rootGeneration == 25 && entry.has("structure_plan25")) fields.add("structure_plan25");
|
||||
requireFields(entry, fields);
|
||||
decoded.add(new ExpansionIsland(string(entry, "id"), string(entry, "parent"), string(entry, "direction"), string(entry, "climate"),
|
||||
integer(entry, "diameter"), integer(entry, "center_x"), integer(entry, "center_z"), number(entry, "seed").longValueExact(),
|
||||
string(entry, "relief"), string(entry, "status"), integer(entry, "generation"),
|
||||
entry.has("reuse_token") ? string(entry, "reuse_token") : "",
|
||||
entry.has("structure_priorities") ? priorities(entry) : List.of()));
|
||||
entry.has("structure_priorities") ? priorities(entry) : List.of(),
|
||||
entry.has("structure_plan25") ? java.util.Optional.of(structurePlan(entry.getAsJsonObject("structure_plan25"))) : java.util.Optional.empty()));
|
||||
}
|
||||
return validate(decoded);
|
||||
} catch (RuntimeException e) {
|
||||
@@ -213,7 +215,9 @@ public final class ExpansionJournal {
|
||||
long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ();
|
||||
if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id());
|
||||
for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id());
|
||||
if (rootGeneration == 24 && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
|
||||
if ((rootGeneration == 24 || rootGeneration == 25) && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
|
||||
try { island.structurePlan25().ifPresent(plan -> plan.validateFor(island)); }
|
||||
catch (RuntimeException invalid) { throw new IOException("Invalid native monument reservation", invalid); }
|
||||
previous.put(island.id(), island);
|
||||
}
|
||||
return checked;
|
||||
@@ -235,8 +239,8 @@ public final class ExpansionJournal {
|
||||
|
||||
private JsonObject encode(List<ExpansionIsland> entries) {
|
||||
JsonObject root = new JsonObject();
|
||||
root.addProperty("schema", rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
|
||||
if (rootGeneration == 24) root.addProperty("aerial_plan", aerialToken);
|
||||
root.addProperty("schema", rootGeneration == 25 ? 3 : rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
|
||||
if ((rootGeneration == 24 || rootGeneration == 25)) root.addProperty("aerial_plan", aerialToken);
|
||||
JsonArray array = new JsonArray();
|
||||
for (ExpansionIsland island : entries) {
|
||||
JsonObject entry = new JsonObject();
|
||||
@@ -250,6 +254,8 @@ public final class ExpansionJournal {
|
||||
var priorities = new JsonArray(); island.structurePriorities().forEach(priorities::add);
|
||||
entry.add("structure_priorities", priorities);
|
||||
}
|
||||
island.structurePlan25().ifPresent(plan -> entry.add("structure_plan25", ExpansionStructurePlan25.CODEC
|
||||
.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow()));
|
||||
array.add(entry);
|
||||
}
|
||||
root.add("islands", array);
|
||||
@@ -258,7 +264,7 @@ public final class ExpansionJournal {
|
||||
|
||||
private void bindAerial(String token) throws IOException {
|
||||
if (!aerialToken.isEmpty() && !aerialToken.equals(token)) throw new IOException("Aerial manifest identity changed outside this journal");
|
||||
var stored = AerialStore24.load(AerialStore24.directory(directory), token, seed, rootDiameter);
|
||||
var stored = AerialStore24.load(AerialStore24.directory(directory, rootGeneration), token, seed, rootDiameter, rootGeneration);
|
||||
if (!aerialToken.isEmpty() && !aerialPlan24.sites().equals(stored.plan().sites())) throw new IOException("Aerial layout changed outside this journal");
|
||||
aerialToken = token;
|
||||
aerialPlan24 = stored.plan();
|
||||
@@ -296,6 +302,10 @@ public final class ExpansionJournal {
|
||||
private static void requireFields(JsonObject value, Set<String> fields) throws IOException {
|
||||
if (!value.keySet().equals(fields)) throw new IOException("Missing or unknown expansion journal fields");
|
||||
}
|
||||
private static ExpansionStructurePlan25 structurePlan(JsonObject value) throws IOException {
|
||||
requireFields(value, Set.of("algorithm", "structure", "chunk_x", "chunk_z", "min_x", "min_y", "min_z", "max_x", "max_y", "max_z", "pieces", "digest"));
|
||||
return ExpansionStructurePlan25.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, value).getOrThrow();
|
||||
}
|
||||
private static List<String> priorities(JsonObject value) throws IOException {
|
||||
var array = value.get("structure_priorities");
|
||||
if (!array.isJsonArray()) throw new IOException("Structure priorities must be an array");
|
||||
|
||||
@@ -24,14 +24,14 @@ public final class ExpansionOcean23 {
|
||||
}
|
||||
private ExpansionOcean23() {}
|
||||
public static boolean active(ExpansionIsland island) {
|
||||
return island != null && (island.generation() == 23 || island.generation() == 24) && !island.origin() && island.relief().equals("ocean");
|
||||
return island != null && (island.generation() == 23 || island.generation() == 24 || island.generation() == 25) && !island.origin() && island.relief().equals("ocean");
|
||||
}
|
||||
public static int seaLevel(ExpansionIsland island) {
|
||||
return (int) Math.round(ExpansionRelief.worldY(island, 220));
|
||||
}
|
||||
/** Quick expansions retain their size/climate draw; only unspecified relief participates. */
|
||||
public static String quickRelief(long seed, String id, int diameter, String climate, int generation) {
|
||||
if ((generation != 23 && generation != 24) || diameter < 512 || climate.equals("lost_city")) return "natural";
|
||||
if ((generation != 23 && generation != 24 && generation != 25) || diameter < 512 || climate.equals("lost_city")) return "natural";
|
||||
return Long.remainderUnsigned(ExpansionIsland.deriveSeed(seed ^ 0x0CEA231L, id), 3) == 0 ? "ocean" : "natural";
|
||||
}
|
||||
public static Column column(ExpansionIsland island, int x, int z) {
|
||||
|
||||
@@ -41,18 +41,25 @@ public final class ExpansionRuntime {
|
||||
&& !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS))
|
||||
throw new IOException("Initialized laboratory world is missing its expansion journal directory");
|
||||
String initialAerialToken = "";
|
||||
if (generator.rootGeneration() == 24 && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
|
||||
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory), level.getSeed(), generator.initialDiameter(),
|
||||
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory, generator.rootGeneration()), level.getSeed(), generator.initialDiameter(), generator.rootGeneration(),
|
||||
() -> AerialPlanner24.forWorld(level, sanctuary)).token();
|
||||
}
|
||||
var journal = generator.rootGeneration() == 24
|
||||
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 24, initialAerialToken)
|
||||
var journal = (generator.rootGeneration() == 24 || generator.rootGeneration() == 25)
|
||||
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration(), initialAerialToken)
|
||||
: new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
|
||||
if (generator.rootGeneration() == 24) {
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
|
||||
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
|
||||
sanctuary.setAerialPlan24(journal.aerialPlan24());
|
||||
}
|
||||
for (var island : journal.islands()) if (island.structurePlan25().isPresent())
|
||||
island.structurePlan25().get().restore(net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext.fromLevel(level), island);
|
||||
if (generator.rootGeneration() == 25 && generator instanceof SanctuaryChunkGenerator sanctuary) {
|
||||
// Reload has no aerial planning callback: attach the same immutable root sampler before checking future water.
|
||||
sanctuary.prepareAerialSampling24(level);
|
||||
ExpansionInitialFlows25.prepare(sanctuary);
|
||||
}
|
||||
ExpansionVoidReuse.open(level, generator, journal.islands());
|
||||
generator.initialize(level, journal.islands());
|
||||
var session = new Session(level, generator, journal);
|
||||
@@ -124,15 +131,19 @@ public final class ExpansionRuntime {
|
||||
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
|
||||
// Capture one immutable decision before any candidate is probed or certified. A resumed
|
||||
// reservation uses its recorded profile even if the world's census has since changed.
|
||||
var priorities = generator.rootGeneration() == 23 || generator.rootGeneration() == 24
|
||||
var priorities = generator.rootGeneration() == 23 || (generator.rootGeneration() == 24 || generator.rootGeneration() == 25)
|
||||
? fr.koka.sanctuary.knowledge.structures.StructureCensusService.priorities(level) : List.<String>of();
|
||||
var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance)
|
||||
.stream().filter(value -> !aerialConflicts(value)).map(value -> value.withStructurePriorities(priorities)).toList();
|
||||
boolean nativeRefused = false;
|
||||
for (var value : candidates) {
|
||||
for (var candidate : candidates) {
|
||||
ExpansionIsland value = candidate;
|
||||
ExpansionAdmission.checkDeadline(deadline);
|
||||
try {
|
||||
if (value.generation() == 23 || value.generation() == 24) ExpansionStructures23.verifyExpansion(level, value, deadline);
|
||||
if (value.generation() == 25) {
|
||||
value = ExpansionStructures23.prepareExpansion25(level, value, deadline);
|
||||
if (!value.relief().equals("ocean")) ExpansionStructures23.verifyExpansion(level, value, deadline);
|
||||
} else if (value.generation() == 23 || value.generation() == 24) ExpansionStructures23.verifyExpansion(level, value, deadline);
|
||||
var inspection = ExpansionIsland.spatialGeneration(value.generation()) ? ExpansionVoidReuse.inspect(level, value, deadline) : null;
|
||||
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
|
||||
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
|
||||
@@ -153,14 +164,15 @@ public final class ExpansionRuntime {
|
||||
nativeRefused = true; // Try another location with the same immutable priorities.
|
||||
} finally {
|
||||
generator.discardUnpublishedContext(value);
|
||||
if (!value.equals(candidate)) generator.discardUnpublishedContext(candidate);
|
||||
}
|
||||
}
|
||||
ExpansionAdmission.checkDeadline(deadline);
|
||||
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
|
||||
}
|
||||
private boolean aerialConflicts(ExpansionIsland island) {
|
||||
return generator.rootGeneration() == 24 && generator instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& sanctuary.aerialPlan24().conflicts(island);
|
||||
return (generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.aerialPlan24().conflicts(island) || ExpansionInitialFlows25.conflicts(sanctuary, island));
|
||||
}
|
||||
public void resume(String id) {
|
||||
requireHealthyJournal();
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtAccounter;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
/** Immutable native monument selected before an ocean25 reservation. No block, entity or chunk is stored here. */
|
||||
public record ExpansionStructurePlan25(String algorithm, String structure, int chunkX, int chunkZ,
|
||||
int minX, int minY, int minZ, int maxX, int maxY, int maxZ, String pieces, String digest) {
|
||||
public static final String ALGORITHM = "ocean_monument25_v1";
|
||||
public static final int MAX_NBT_BYTES = 65_536;
|
||||
public static final Codec<ExpansionStructurePlan25> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||
Codec.STRING.fieldOf("algorithm").forGetter(ExpansionStructurePlan25::algorithm),
|
||||
Codec.STRING.fieldOf("structure").forGetter(ExpansionStructurePlan25::structure),
|
||||
Codec.INT.fieldOf("chunk_x").forGetter(ExpansionStructurePlan25::chunkX),
|
||||
Codec.INT.fieldOf("chunk_z").forGetter(ExpansionStructurePlan25::chunkZ),
|
||||
Codec.INT.fieldOf("min_x").forGetter(ExpansionStructurePlan25::minX),
|
||||
Codec.INT.fieldOf("min_y").forGetter(ExpansionStructurePlan25::minY),
|
||||
Codec.INT.fieldOf("min_z").forGetter(ExpansionStructurePlan25::minZ),
|
||||
Codec.INT.fieldOf("max_x").forGetter(ExpansionStructurePlan25::maxX),
|
||||
Codec.INT.fieldOf("max_y").forGetter(ExpansionStructurePlan25::maxY),
|
||||
Codec.INT.fieldOf("max_z").forGetter(ExpansionStructurePlan25::maxZ),
|
||||
Codec.STRING.fieldOf("pieces").forGetter(ExpansionStructurePlan25::pieces),
|
||||
Codec.STRING.fieldOf("digest").forGetter(ExpansionStructurePlan25::digest)
|
||||
).apply(i, ExpansionStructurePlan25::new));
|
||||
|
||||
public ExpansionStructurePlan25 {
|
||||
if (!ALGORITHM.equals(algorithm) || !"minecraft:monument".equals(structure))
|
||||
throw new IllegalArgumentException("Unsupported saved native structure algorithm");
|
||||
if (Math.abs((long) chunkX) > 6_300 || Math.abs((long) chunkZ) > 6_300
|
||||
|| minX > maxX || minY > maxY || minZ > maxZ || minY < 3 || maxY > 379
|
||||
|| (long) maxX - minX > 256 || (long) maxZ - minZ > 256
|
||||
|| minX < (long) chunkX * 16 - 128 || maxX > (long) chunkX * 16 + 143
|
||||
|| minZ < (long) chunkZ * 16 - 128 || maxZ > (long) chunkZ * 16 + 143)
|
||||
throw new IllegalArgumentException("Invalid saved native structure bounds");
|
||||
if (pieces == null || pieces.isEmpty() || pieces.length() > (MAX_NBT_BYTES + 2) / 3 * 4
|
||||
|| digest == null || !digest.matches("[0-9a-f]{64}") || !ExpansionVoidCertificate.digest(pieces).equals(digest))
|
||||
throw new IllegalArgumentException("Invalid saved native structure checksum");
|
||||
CompoundTag tag = decode(pieces);
|
||||
if (!structure.equals(tag.getStringOr("id", "")) || tag.getIntOr("ChunkX", Integer.MIN_VALUE) != chunkX
|
||||
|| tag.getIntOr("ChunkZ", Integer.MIN_VALUE) != chunkZ || tag.getIntOr("references", -1) != 0
|
||||
|| tag.getListOrEmpty("Children").isEmpty())
|
||||
throw new IllegalArgumentException("Saved native structure identity differs from its pieces");
|
||||
}
|
||||
|
||||
public static ExpansionStructurePlan25 capture(StructurePieceSerializationContext context, StructureStart start) {
|
||||
if (!start.isValid() || start.getReferences() != 0) throw new IllegalArgumentException("Expected an unreferenced native start");
|
||||
return fromTag(start.getBoundingBox(), start.createTag(context, start.getChunkPos()));
|
||||
}
|
||||
|
||||
/** Also used by serialization tests without a Minecraft world. */
|
||||
static ExpansionStructurePlan25 fromTag(BoundingBox bounds, CompoundTag tag) {
|
||||
try {
|
||||
var bytes = new ByteArrayOutputStream();
|
||||
NbtIo.write(tag, new DataOutputStream(bytes));
|
||||
if (bytes.size() > MAX_NBT_BYTES) throw new IllegalArgumentException("Native monument exceeds its saved-piece budget");
|
||||
String encoded = Base64.getEncoder().encodeToString(bytes.toByteArray());
|
||||
return new ExpansionStructurePlan25(ALGORITHM, tag.getStringOr("id", ""), tag.getIntOr("ChunkX", 0), tag.getIntOr("ChunkZ", 0),
|
||||
bounds.minX(), bounds.minY(), bounds.minZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ(), encoded, ExpansionVoidCertificate.digest(encoded));
|
||||
} catch (IOException error) { throw new IllegalArgumentException("Cannot encode native monument", error); }
|
||||
}
|
||||
|
||||
public ChunkPos startChunk() { return new ChunkPos(chunkX, chunkZ); }
|
||||
public BoundingBox bounds() { return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ); }
|
||||
|
||||
/** Includes native foundation/clearing work around the visible body. */
|
||||
public BoundingBox protection() { return new BoundingBox(minX - 16, Math.max(0, minY - 16), minZ - 16, maxX + 16, maxY + 8, maxZ + 16); }
|
||||
public boolean conflicts(BoundingBox other) { return protection().intersects(other); }
|
||||
public boolean protects(net.minecraft.core.BlockPos pos) {
|
||||
return pos.getX() >= minX - 16 && pos.getX() <= maxX + 16 && pos.getZ() >= minZ - 16 && pos.getZ() <= maxZ + 16
|
||||
&& pos.getY() >= Math.max(0, minY - 16) && pos.getY() <= maxY + 8;
|
||||
}
|
||||
|
||||
public void validateFor(ExpansionIsland island) {
|
||||
if (island.generation() != 25 || island.origin() || !island.relief().equals("ocean")
|
||||
|| !island.ownsChunk(chunkX, chunkZ)) throw new IllegalArgumentException("Native monument belongs to an ocean25 expansion");
|
||||
for (int x : new int[]{minX, maxX}) for (int z : new int[]{minZ, maxZ})
|
||||
if (!island.contains(x, z)) throw new IllegalArgumentException("Saved monument extends outside its island");
|
||||
CompoundTag metadata = tag().getCompoundOrEmpty("sanctuary_native");
|
||||
if (metadata.getIntOr("generation", 0) != 23 || metadata.getLongOr("island_seed", 0) != island.seed()
|
||||
|| metadata.getIntOr("sea_level", -64) != ExpansionOcean23.seaLevel(island))
|
||||
throw new IllegalArgumentException("Saved monument seed or sea level differs from its island");
|
||||
}
|
||||
|
||||
public StructureStart restore(StructurePieceSerializationContext context, ExpansionIsland island) {
|
||||
validateFor(island);
|
||||
CompoundTag saved = tag();
|
||||
StructureStart result = StructureStart.loadStaticStart(context, saved, island.seed());
|
||||
if (result == null || !result.isValid() || !bounds().equals(result.getBoundingBox())
|
||||
|| !startChunk().equals(result.getChunkPos()) || !saved.equals(result.createTag(context, result.getChunkPos())))
|
||||
throw new IllegalStateException("Reserved native monument no longer decodes exactly; refusing a new draw");
|
||||
return result;
|
||||
}
|
||||
|
||||
public CompoundTag tag() { return decode(pieces); }
|
||||
private static CompoundTag decode(String encoded) {
|
||||
try {
|
||||
byte[] bytes = Base64.getDecoder().decode(encoded);
|
||||
if (bytes.length > MAX_NBT_BYTES) throw new IllegalArgumentException("Native monument exceeds its byte budget");
|
||||
var input = new DataInputStream(new ByteArrayInputStream(bytes));
|
||||
var tag = NbtIo.read(input, NbtAccounter.create(1_048_576));
|
||||
if (input.available() != 0) throw new IllegalArgumentException("Trailing bytes in saved native monument");
|
||||
return tag;
|
||||
} catch (IOException error) { throw new IllegalArgumentException("Cannot decode saved native monument", error); }
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public final class ExpansionStructures {
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
|
||||
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
|
||||
return;
|
||||
}
|
||||
|
||||
+148
-11
@@ -61,7 +61,7 @@ public final class ExpansionStructures23 {
|
||||
public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); }
|
||||
public static String family(String id) { return ExpansionStructureCatalog23.family(id); }
|
||||
public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; }
|
||||
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24; }
|
||||
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24 || generation == 25; }
|
||||
public record Candidate(ChunkPos chunk, long roll) {}
|
||||
public record Accepted(Holder<Structure> structure, StructureStart start) {}
|
||||
public static final class NoSuitableStructureException extends IllegalArgumentException {
|
||||
@@ -75,6 +75,8 @@ public final class ExpansionStructures23 {
|
||||
int rootX = island.centerX() >> 4, rootZ = island.centerZ() >> 4;
|
||||
var central = new ChunkPos(rootX, rootZ);
|
||||
result.put(central.pack(), new Candidate(central, mix(island.seed() ^ 0xA2300C37L)));
|
||||
island.structurePlan25().ifPresent(saved -> result.putIfAbsent(saved.startChunk().pack(),
|
||||
new Candidate(saved.startChunk(), mix(island.seed() ^ saved.startChunk().pack() ^ 0xA25C37L))));
|
||||
int cells = (island.terrainRadius() + 127) / 128;
|
||||
for (int cz = -cells; cz <= cells; cz++) for (int cx = -cells; cx <= cells; cx++) {
|
||||
long roll = mix(island.seed() ^ 0xA23CA7A10L ^ cx * 0x9e3779b97f4a7c15L ^ cz * 0xc2b2ae3d27d4eb4fL);
|
||||
@@ -92,12 +94,23 @@ public final class ExpansionStructures23 {
|
||||
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
|
||||
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (island == null || !nativeGeneration(island.generation())) return;
|
||||
if (island.origin() && island.generation() == 25 && generator instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).starts().stream()
|
||||
.anyMatch(site -> site.start().getChunkPos().equals(chunk.getPos()))) return;
|
||||
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
|
||||
if (candidate == null) return;
|
||||
for (var old : chunk.getAllStarts().values()) {
|
||||
if (old.isValid() && ((Object) old) instanceof NativeStructureMetadata23 metadata
|
||||
&& metadata.sanctuary$native23() != null) return;
|
||||
}
|
||||
if (island.structurePlan25().isPresent() && island.structurePlan25().get().startChunk().equals(chunk.getPos())) {
|
||||
var saved = island.structurePlan25().get();
|
||||
var serialization = new net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext(
|
||||
net.minecraft.server.packs.resources.ResourceManager.Empty.INSTANCE, registries, templates);
|
||||
StructureStart start = saved.restore(serialization, island);
|
||||
manager.setStartForStructure(start.getStructure(), start, chunk);
|
||||
return;
|
||||
}
|
||||
var view = new NativeView(generator, island, generator.source(), registries);
|
||||
Accepted accepted = plan(generator, view, island, candidate, registries, templates, dimension, chunk, Long.MAX_VALUE);
|
||||
if (accepted != null) {
|
||||
@@ -112,6 +125,10 @@ public final class ExpansionStructures23 {
|
||||
*/
|
||||
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
|
||||
if (!nativeGeneration(island.generation()) || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
|
||||
if (island.generation() == 25 && island.relief().equals("ocean")) {
|
||||
prepareExpansion25(level, island, deadlineNanos);
|
||||
return;
|
||||
}
|
||||
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host))
|
||||
throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world");
|
||||
ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess());
|
||||
@@ -126,11 +143,28 @@ public final class ExpansionStructures23 {
|
||||
throw new NoSuitableStructureException();
|
||||
}
|
||||
|
||||
/** The selected native start becomes part of the journal/certificate identity before any terrain inspection. */
|
||||
public static ExpansionIsland prepareExpansion25(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
|
||||
if (island.generation() != 25 || island.origin() || !island.relief().equals("ocean")
|
||||
|| !level.structureManager().shouldGenerateStructures()) return island;
|
||||
var context = net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext.fromLevel(level);
|
||||
if (island.structurePlan25().isPresent()) {
|
||||
island.structurePlan25().get().restore(context, island);
|
||||
return island;
|
||||
}
|
||||
Accepted accepted = preview(level, island, "minecraft:monument", deadlineNanos);
|
||||
if (accepted == null) throw new NoSuitableStructureException();
|
||||
var result = island.withStructurePlan25(ExpansionStructurePlan25.capture(context, accepted.start()));
|
||||
// A native monument recreates its rooms from seed when loading. Verify that path before accepting it.
|
||||
result.structurePlan25().orElseThrow().restore(context, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Public for native diagnostics: only accepted starts are returned; no registry or chunk mutation. */
|
||||
public static Accepted plan(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
|
||||
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
|
||||
ChunkAccess chunk, long deadlineNanos) {
|
||||
try { return planFiltered(host, view, island, candidate, registries, templates, dimension, chunk, deadlineNanos, entry -> true); }
|
||||
try { return planFiltered(host, view, island, candidate, registries, templates, dimension, chunk, deadlineNanos, entry -> generalSelection(island, entry)); }
|
||||
catch (SampleBudgetExceeded refused) { diagnostic(island, candidate, "terrain-budget-refused", ""); return null; }
|
||||
}
|
||||
|
||||
@@ -142,6 +176,53 @@ public final class ExpansionStructures23 {
|
||||
public static Accepted previewSelected(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos) {
|
||||
return preview(level, island, structureId, deadlineNanos, true);
|
||||
}
|
||||
/** One explicit candidate for root25 conditional planners; no chunk request or publication. */
|
||||
public static Accepted previewAt(ServerLevel level, ExpansionIsland island, String structureId, Candidate candidate, long deadlineNanos) {
|
||||
var host = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
|
||||
return previewAt(level, host, island, structureId, candidate, deadlineNanos);
|
||||
}
|
||||
/** Same admission with an explicit, already prepared host, including isolated native diagnostics. */
|
||||
public static Accepted previewAt(ServerLevel level, ExpansionChunkGenerator host, ExpansionIsland island,
|
||||
String structureId, Candidate candidate, long deadlineNanos) {
|
||||
var view = new NativeView(host, island, isolatedSource(host, island, level.registryAccess()), level.registryAccess());
|
||||
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||
try {
|
||||
return planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
|
||||
level.dimension(), chunk, deadlineNanos, entry -> entry.id().equals(structureId));
|
||||
} catch (SampleBudgetExceeded refused) { return null; }
|
||||
}
|
||||
/** Only the initial25 conditional mine planner uses piece-wise hydrology; historical/general admission stays unchanged. */
|
||||
public static Accepted previewMine25(ServerLevel level, SanctuaryChunkGenerator host, ExpansionIsland island,
|
||||
Candidate candidate, long deadlineNanos) {
|
||||
if (!island.origin() || island.generation() != 25 || host.rootGeneration() != 25)
|
||||
throw new IllegalArgumentException("The conditional mine preview requires the initial25 island");
|
||||
var view = new NativeView(host, island, isolatedSource(host, island, level.registryAccess()), level.registryAccess(), Integer.MAX_VALUE, true);
|
||||
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||
try {
|
||||
return planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
|
||||
level.dimension(), chunk, deadlineNanos, entry -> entry.id().equals("minecraft:mineshaft"));
|
||||
} catch (SampleBudgetExceeded refused) { return null; }
|
||||
}
|
||||
/** Lower25 marshes use their actual local shore, while all certification columns remain complete. */
|
||||
public static Accepted previewAtWetland25(ServerLevel level, SanctuaryChunkGenerator host, ExpansionIsland island,
|
||||
Candidate candidate, fr.koka.sanctuary.worldgen.Terrain25Wetlands.Column shore, long deadlineNanos) {
|
||||
if (!island.origin() || island.generation() != 25 || host.rootGeneration() != 25 || !shore.underground())
|
||||
throw new IllegalArgumentException("A lower wetland preview requires an initial25 underground marsh");
|
||||
int x = candidate.chunk().getMiddleBlockX(), z = candidate.chunk().getMiddleBlockZ();
|
||||
if (shore.x() != x || shore.z() != z || !fr.koka.sanctuary.worldgen.Terrain25Runtime.wetlandAt(
|
||||
host.delegate(island), host.randomState(island), x, shore.floorY() + 1, z)) return null;
|
||||
int ceiling = shore.waterY() + 4;
|
||||
var view = new NativeView(host, island, isolatedSource(host, island, level.registryAccess()), level.registryAccess(), ceiling);
|
||||
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||
var column = view.getBaseColumn(x, z, chunk, view.randomState(island));
|
||||
if (new Ground(column, chunk.getMinY(), ceiling).top != shore.floorY()) {
|
||||
diagnostic(island, candidate, "hut25:local-shore-height-mismatch", "minecraft:swamp_hut"); return null;
|
||||
}
|
||||
try {
|
||||
return planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
|
||||
level.dimension(), chunk, deadlineNanos, entry -> entry.id().equals("minecraft:swamp_hut"));
|
||||
} catch (SampleBudgetExceeded refused) { return null; }
|
||||
}
|
||||
private static Accepted preview(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos, boolean selectedOnly) {
|
||||
var host = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
|
||||
ExpansionBiomeSource source = isolatedSource(host, island, level.registryAccess());
|
||||
@@ -152,13 +233,18 @@ public final class ExpansionStructures23 {
|
||||
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, factory, null);
|
||||
try {
|
||||
Accepted accepted = planFiltered(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(),
|
||||
level.dimension(), chunk, deadlineNanos, entry -> selectedOnly || entry.id().equals(structureId));
|
||||
level.dimension(), chunk, deadlineNanos, entry -> selectedOnly ? generalSelection(island, entry) : entry.id().equals(structureId));
|
||||
if (accepted != null && accepted.structure().unwrapKey().orElseThrow().identifier().toString().equals(structureId)) return accepted;
|
||||
} catch (SampleBudgetExceeded refused) { /* An unsuitable candidate does not suppress the following sites. */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The initial25 village belongs to the conditional underground planner; expansion and explicit preview catalogues stay complete. */
|
||||
private static boolean generalSelection(ExpansionIsland island, Entry entry) {
|
||||
return island.generation() != 25 || !island.origin() || !entry.family().equals("village");
|
||||
}
|
||||
|
||||
private static Accepted planFiltered(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
|
||||
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
|
||||
ChunkAccess chunk, long deadlineNanos, Predicate<Entry> included) {
|
||||
@@ -258,7 +344,11 @@ public final class ExpansionStructures23 {
|
||||
for (StructurePiece piece : pieces) {
|
||||
var box = piece.getBoundingBox(); int sum = 0, count = 0, lowest = Integer.MAX_VALUE;
|
||||
for (int z = box.minZ(); z <= box.maxZ(); z++) for (int x = box.minX(); x <= box.maxX(); x++) {
|
||||
int height = ground.at(x, z).top + 1; sum += height; count++; lowest = Math.min(lowest, height);
|
||||
int height = entry.family().equals("swamp_hut") && ground.localSurfaceCeiling != Integer.MAX_VALUE
|
||||
? fr.koka.sanctuary.worldgen.underground25.SwampHutAdmission25.localNativeHeight(
|
||||
ground.at(x, z).column, ground.level.getMinY(), ground.localSurfaceCeiling)
|
||||
: ground.at(x, z).top + 1;
|
||||
sum += height; count++; lowest = Math.min(lowest, height);
|
||||
}
|
||||
int height = entry.family().equals("desert_pyramid") ? lowest : sum / count;
|
||||
int burial = entry.family().equals("desert_pyramid")
|
||||
@@ -308,6 +398,13 @@ public final class ExpansionStructures23 {
|
||||
private static String supportRejection(ExpansionChunkGenerator host, ExpansionIsland island, Entry entry,
|
||||
StructureStart start, GroundSampler ground) {
|
||||
BoundingBox total = start.getBoundingBox();
|
||||
if (island.origin() && island.generation() == 25 && host instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).intersects(
|
||||
new BoundingBox(total.minX() - 8, total.minY() - 16, total.minZ() - 8, total.maxX() + 8, total.maxY() + 8, total.maxZ() + 8)))
|
||||
return "reserved-underground25";
|
||||
if (island.structurePlan25().isPresent() && island.structurePlan25().get().conflicts(
|
||||
new BoundingBox(total.minX() - 8, total.minY() - 16, total.minZ() - 8, total.maxX() + 8, total.maxY() + 8, total.maxZ() + 8)))
|
||||
return "reserved-monument25";
|
||||
if (island.origin() && entry.habitat() != Habitat.UNDERGROUND && !undergroundPortal(start)) {
|
||||
int nearestX = Math.clamp(island.centerX(), total.minX(), total.maxX());
|
||||
int nearestZ = Math.clamp(island.centerZ(), total.minZ(), total.maxZ());
|
||||
@@ -335,7 +432,17 @@ public final class ExpansionStructures23 {
|
||||
|| city.intersectsExperimentalStructures(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)) return "reserved-secret";
|
||||
if (island.origin()) {
|
||||
var binding = city.starterContext();
|
||||
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
|
||||
if (island.generation() == 25 && entry.family().equals("swamp_hut")) {
|
||||
String shore = fr.koka.sanctuary.worldgen.underground25.SwampHutAdmission25.rejection(city, island, start,
|
||||
(x, z) -> ground.at(x, z).column, ground.localSurfaceCeiling != Integer.MAX_VALUE);
|
||||
if (shore != null) return shore;
|
||||
} else if (island.generation() == 25 && entry.family().equals("mineshaft") && ground.preciseMineHydrology25) {
|
||||
if (ground.mineHydrology25 == null) ground.mineHydrology25 = new fr.koka.sanctuary.worldgen.underground25.MineHydrologyAdmission25(binding);
|
||||
var check = ground.mineHydrology25.check(start);
|
||||
if (DIAGNOSTICS) SanctuaryMod.LOGGER.info("Native25 mine hydrology seed {} chunk {} pieces {} probes {} refusal {} at {}",
|
||||
island.seed(), start.getChunkPos(), start.getPieces().size(), check.probes(), check.rejection(), check.at());
|
||||
if (check.rejection() != null) return check.rejection();
|
||||
} else if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
|
||||
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
|
||||
}
|
||||
}
|
||||
@@ -400,17 +507,21 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
}
|
||||
if (sanctuaryRequested && generator instanceof SanctuaryChunkGenerator sanctuary) addSanctuaryCandidates(sanctuary, candidates);
|
||||
if (generator.rootGeneration() == 25 && nativeRequested && generator instanceof SanctuaryChunkGenerator sanctuary)
|
||||
for (var site : fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(sanctuary).starts())
|
||||
addPosition(candidates, site.start().getChunkPos().x(), site.start().getChunkPos().z());
|
||||
candidates.removeIf(c -> Math.abs((long) c.chunk().x() - (origin.getX() >> 4)) > (long) radius * CELL_CHUNKS
|
||||
|| Math.abs((long) c.chunk().z() - (origin.getZ() >> 4)) > (long) radius * CELL_CHUNKS);
|
||||
candidates = new ArrayList<>(candidates.stream().collect(java.util.stream.Collectors.toMap(
|
||||
c -> c.chunk().pack(), c -> c, (first, second) -> first)).values());
|
||||
candidates.sort(Comparator.comparingDouble((Candidate c) -> horizontalDistanceSquared(origin, c.chunk().getMiddleBlockX(), c.chunk().getMiddleBlockZ()))
|
||||
.thenComparingInt(c -> c.chunk().x()).thenComparingInt(c -> c.chunk().z()));
|
||||
Holder<Structure> bestHolder = null; StructureStart best = null; BlockPos bestPos = null; double bestDistance = Double.POSITIVE_INFINITY;
|
||||
Holder<Structure> bestHolder = null; StructureStart best = null; ChunkAccess bestChunk = null;
|
||||
BlockPos bestPos = null; double bestDistance = Double.POSITIVE_INFINITY;
|
||||
for (Candidate candidate : candidates) {
|
||||
// Every admitted start is within the native 128-block reference range of its start chunk.
|
||||
double minimumDistance = Math.max(0, Math.sqrt(horizontalDistanceSquared(origin, candidate.chunk().getMiddleBlockX(), candidate.chunk().getMiddleBlockZ())) - 192);
|
||||
if (!sanctuaryRequested && minimumDistance * minimumDistance > bestDistance) break;
|
||||
if (!sanctuaryRequested && generator.rootGeneration() != 25 && minimumDistance * minimumDistance > bestDistance) break;
|
||||
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
for (Holder<Structure> holder : wanted) {
|
||||
Entry entry = ExpansionStructureCatalog23.entry(holder.unwrapKey().orElseThrow().identifier().toString());
|
||||
@@ -419,12 +530,17 @@ public final class ExpansionStructures23 {
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
BlockPos position = locator(start, origin);
|
||||
double distance = horizontalDistanceSquared(origin, position.getX(), position.getZ());
|
||||
if (distance < bestDistance) { best = start; bestPos = position; bestHolder = holder; bestDistance = distance; }
|
||||
if (distance < bestDistance) { best = start; bestChunk = chunk; bestPos = position; bestHolder = holder; bestDistance = distance; }
|
||||
}
|
||||
}
|
||||
if (best == null) return null;
|
||||
// Mutate only the final winner, including when the request is a tag containing several structures.
|
||||
if (skipReferenced) level.structureManager().addReference(best);
|
||||
if (skipReferenced) {
|
||||
level.structureManager().addReference(best);
|
||||
// Vanilla updates the start and its in-memory presence cache, not the
|
||||
// chunk's save flag. A distant READY chunk may already be clean.
|
||||
bestChunk.markUnsaved();
|
||||
}
|
||||
return Pair.of(bestPos, bestHolder);
|
||||
}
|
||||
|
||||
@@ -440,7 +556,7 @@ public final class ExpansionStructures23 {
|
||||
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||
var mineral = generator.mineralSanctuary23();
|
||||
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
|
||||
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
|
||||
if (generator.rootGeneration() == 24 || generator.rootGeneration() == 25) generator.aerialPlan24().sites()
|
||||
.forEach(site -> addPosition(positions, site.startChunkX(), site.startChunkZ()));
|
||||
}
|
||||
private static void addPosition(List<Candidate> positions, int x, int z) { positions.add(new Candidate(new ChunkPos(x, z), 0)); }
|
||||
@@ -505,17 +621,22 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
private static final class GroundSampler {
|
||||
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
|
||||
final int localSurfaceCeiling;
|
||||
final boolean preciseMineHydrology25;
|
||||
fr.koka.sanctuary.worldgen.underground25.MineHydrologyAdmission25 mineHydrology25;
|
||||
final Map<Long, Ground> columns = new HashMap<>();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityProtection16 localCity;
|
||||
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline) {
|
||||
this.generator = generator; this.level = level; this.random = random; this.deadline = deadline;
|
||||
localSurfaceCeiling = generator instanceof NativeView view ? view.localSurfaceCeiling : Integer.MAX_VALUE;
|
||||
preciseMineHydrology25 = generator instanceof NativeView view && view.preciseMineHydrology25;
|
||||
}
|
||||
Ground at(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
Ground old = columns.get(key); if (old != null) return old;
|
||||
checkDeadline(deadline);
|
||||
if (columns.size() >= MAX_COLUMN_SAMPLES) throw new SampleBudgetExceeded();
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), level.getMaxY());
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), Math.min(level.getMaxY(), localSurfaceCeiling));
|
||||
columns.put(key, result); return result;
|
||||
}
|
||||
}
|
||||
@@ -536,14 +657,30 @@ public final class ExpansionStructures23 {
|
||||
/** Isolated biome snapshot during preflight, per-island sea level during native generation. */
|
||||
private static final class NativeView extends ExpansionChunkGenerator {
|
||||
private final ExpansionChunkGenerator host; private final ExpansionIsland island;
|
||||
private final int localSurfaceCeiling;
|
||||
private final boolean preciseMineHydrology25;
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
|
||||
this(host, island, source, registries, Integer.MAX_VALUE);
|
||||
}
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries, int localSurfaceCeiling) {
|
||||
this(host, island, source, registries, localSurfaceCeiling, false);
|
||||
}
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries,
|
||||
int localSurfaceCeiling, boolean preciseMineHydrology25) {
|
||||
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
|
||||
this.localSurfaceCeiling = localSurfaceCeiling; this.preciseMineHydrology25 = preciseMineHydrology25;
|
||||
}
|
||||
@Override public int rootGeneration() { return island.generation(); }
|
||||
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
|
||||
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
|
||||
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
|
||||
if (localSurfaceCeiling != Integer.MAX_VALUE) {
|
||||
var column = getBaseColumn(x, z, level, random);
|
||||
for (int y = Math.min(level.getMaxY(), localSurfaceCeiling); y >= level.getMinY(); y--)
|
||||
if (type.isOpaque().test(column.getBlock(y))) return y + 1;
|
||||
return level.getMinY();
|
||||
}
|
||||
return island.origin() ? host.getBaseHeight(x, z, type, level, random) : super.getBaseHeight(x, z, type, level, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
|
||||
|
||||
@@ -86,9 +86,12 @@ public final class ExpansionVoidReuse {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||
|| !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin())
|
||||
throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion");
|
||||
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
|
||||
&& sanctuary.aerialPlan24().conflicts(island))
|
||||
throw new ExpansionAdmission.OccupiedException("aerial_reserved: expansion overlaps a permanent initial site");
|
||||
if (generator.rootGeneration() == 25 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
|
||||
&& ExpansionInitialFlows25.conflicts(sanctuary, island))
|
||||
throw new ExpansionAdmission.OccupiedException("initial_waterfall_reserved: expansion overlaps a future initial river outlet");
|
||||
List<ChunkPos> positions = positions(island);
|
||||
var entityStorage = entityRegion(level);
|
||||
var poiStorage = ((ExpansionVoidPoiStorageAccess) level.getPoiManager()).sanctuary$simpleRegionStorage();
|
||||
@@ -176,9 +179,12 @@ public final class ExpansionVoidReuse {
|
||||
public static void open(ServerLevel level, ExpansionChunkGenerator generator, List<ExpansionIsland> islands) throws IOException {
|
||||
requireServerThread(level); close(level);
|
||||
if (!ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
|
||||
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
|
||||
for (var island : islands) if (sanctuary.aerialPlan24().conflicts(island))
|
||||
throw new IOException("Saved expansion overlaps a permanent aerial site: " + island.id());
|
||||
if (generator.rootGeneration() == 25 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
|
||||
for (var island : islands) if (ExpansionInitialFlows25.conflicts(sanctuary, island))
|
||||
throw new IOException("Saved expansion overlaps an initial river outlet: " + island.id());
|
||||
var state = new State();
|
||||
for (var island : islands) if (!island.reuseToken().isEmpty()) add(state, loadManifest(level, island));
|
||||
// Validate every transaction before writing even an EMPTY placeholder or publishing any island.
|
||||
|
||||
+4
-3
@@ -84,7 +84,7 @@ public final class StructureCensusService {
|
||||
assertServerThread(level.getServer());
|
||||
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|
||||
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24)
|
||||
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
|
||||
return List.of();
|
||||
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
|
||||
}
|
||||
@@ -157,7 +157,7 @@ public final class StructureCensusService {
|
||||
}).toList();
|
||||
if (matching.isEmpty()) return Presence.OTHER_DIMENSION;
|
||||
// This is an explicit generation23 habitat requirement, not an inferred lack of biomes.
|
||||
if (island != null && (island.generation() == 23 || island.generation() == 24) && !island.relief().equals("ocean")
|
||||
if (island != null && (island.generation() == 23 || island.generation() == 24 || island.generation() == 25) && !island.relief().equals("ocean")
|
||||
&& matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE;
|
||||
return null;
|
||||
}
|
||||
@@ -169,6 +169,7 @@ public final class StructureCensusService {
|
||||
}
|
||||
public static String family(String id) {
|
||||
if (id.equals(StructureCensusMemory.MONSTER_ROOM)) return "monster_room";
|
||||
if (id.equals("sanctuary:underground_abandoned_village")) return "village";
|
||||
return ExpansionStructureCatalog23.entries().stream().filter(entry -> entry.id().equals(id))
|
||||
.map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id);
|
||||
}
|
||||
@@ -179,7 +180,7 @@ public final class StructureCensusService {
|
||||
assertServerThread(level.getServer());
|
||||
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|
||||
|| !(level.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 24) return List.of();
|
||||
|| (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) return List.of();
|
||||
var session = current(level.getServer());
|
||||
var evidence = session.memory.snapshot();
|
||||
String dimension = level.dimension().identifier().toString();
|
||||
|
||||
@@ -31,7 +31,12 @@ public abstract class SurfaceHydrologyMixin {
|
||||
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
|
||||
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) {
|
||||
if (fr.koka.sanctuary.worldgen.Terrain25Runtime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
fr.koka.sanctuary.worldgen.Terrain25Runtime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
}));
|
||||
} else if (PopulationHydrologyRuntime.enabled(generator)) {
|
||||
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
|
||||
PopulationHydrologyRuntime.apply(generator, randomState, generated);
|
||||
return generated;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Only the named generation25 delegate exposes its final water and cave surfaces to admission. */
|
||||
@Mixin(NoiseBasedChunkGenerator.class)
|
||||
public abstract class Terrain25ColumnMixin {
|
||||
@Inject(method = "getBaseColumn", at = @At("RETURN"), cancellable = true)
|
||||
private void sanctuary$finalColumn25(int x, int z, LevelHeightAccessor height, RandomState random,
|
||||
CallbackInfoReturnable<NoiseColumn> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (!Terrain25Runtime.enabled(generator)) return;
|
||||
var column = callback.getReturnValue();
|
||||
Terrain25Runtime.applyColumn(generator, random, x, z, column);
|
||||
fr.koka.sanctuary.worldgen.Terrain25Integration.column(generator, x, z, column);
|
||||
callback.setReturnValue(column);
|
||||
}
|
||||
|
||||
@Inject(method = "getBaseHeight", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$finalHeight25(int x, int z, Heightmap.Types type, LevelHeightAccessor height,
|
||||
RandomState random, CallbackInfoReturnable<Integer> callback) {
|
||||
var generator = (NoiseBasedChunkGenerator) (Object) this;
|
||||
if (!Terrain25Runtime.enabled(generator)) return;
|
||||
var column = generator.getBaseColumn(x, z, height, random);
|
||||
var predicate = type.isOpaque();
|
||||
for (int y = 383; y >= 0; y--) if (predicate.test(column.getBlock(y))) {
|
||||
callback.setReturnValue(y + 1); return;
|
||||
}
|
||||
callback.setReturnValue(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import net.minecraft.util.valueproviders.IntProvider;
|
||||
import net.minecraft.world.level.levelgen.feature.foliageplacers.FoliagePlacer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(FoliagePlacer.class)
|
||||
public interface TreeFoliageOffset25Accessor {
|
||||
@Accessor("offset") IntProvider sanctuary$offset25();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25TreeLimits;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.levelgen.feature.TreeFeature;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(TreeFeature.class)
|
||||
public abstract class TreeHeight25Mixin {
|
||||
/** Vanilla already drew the tree height and any raised root origin; roots and logs have not been placed yet. */
|
||||
@Inject(method = "getMaxFreeTreeHeight", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$wholeTreeBelowCeiling25(WorldGenLevel level, int height, BlockPos trunk,
|
||||
CallbackInfoReturnable<Integer> callback) {
|
||||
if (!(level instanceof WorldGenRegion)
|
||||
|| !(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 25) return;
|
||||
var island = generator.islandAt(trunk.getX(), trunk.getZ());
|
||||
if (island == null || !island.origin()) return;
|
||||
var tree = (TreeFeature) (Object) this;
|
||||
int offset = ((TreeFoliageOffset25Accessor) tree.foliagePlacer()).sanctuary$offset25().maxInclusive();
|
||||
// -1 cannot satisfy vanilla's optional minimum clipped height, so the complete tree is refused.
|
||||
if (!Terrain25TreeLimits.fits(trunk.getY(), height, offset)) callback.setReturnValue(-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlacement25;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Vanilla terrain_matching roads query this at placement time, independently of the planning generator. */
|
||||
@Mixin(WorldGenRegion.class)
|
||||
public abstract class UndergroundHeight25Mixin {
|
||||
@Inject(method = "getHeight", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$undergroundRoad25(Heightmap.Types type, int x, int z, CallbackInfoReturnable<Integer> cir) {
|
||||
Integer floor = UndergroundPlacement25.projectedHeight(x, z);
|
||||
if (floor != null && (type == Heightmap.Types.WORLD_SURFACE_WG || type == Heightmap.Types.OCEAN_FLOOR_WG
|
||||
|| type == Heightmap.Types.WORLD_SURFACE || type == Heightmap.Types.OCEAN_FLOOR)) cir.setReturnValue(floor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlacement25;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
@Mixin(StructureStart.class)
|
||||
public abstract class UndergroundStart25Mixin {
|
||||
@WrapMethod(method = "placeInChunk")
|
||||
private void sanctuary$projectVillage25(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, Operation<Void> original) {
|
||||
UndergroundPlacement25.placing(level, (StructureStart) (Object) this,
|
||||
() -> original.call(level, manager, generator, random, clip, chunk));
|
||||
}
|
||||
}
|
||||
+44
-4
@@ -21,13 +21,22 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
|
||||
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
|
||||
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return; // Vanilla still applies its write-zone and build-height checks.
|
||||
}
|
||||
if (generator.rootGeneration() == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
var center = region.getCenter();
|
||||
var owner = generator.islandForChunk(center.x(), center.z());
|
||||
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
|
||||
if (destination != null && destination.structurePlan25().isPresent()
|
||||
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()
|
||||
&& destination.structurePlan25().orElseThrow().protects(pos)) {
|
||||
callback.setReturnValue(false); return;
|
||||
}
|
||||
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
||||
@@ -44,13 +53,18 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
|
||||
// Vanilla rejects writes outside its region before reading the destination chunk.
|
||||
if (!region.isWithinWriteZone(pos)) return;
|
||||
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if ((generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) {
|
||||
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
|
||||
var island = generator.islandAt(pos.getX(), pos.getZ());
|
||||
if (island != null && island.structurePlan25().isPresent()
|
||||
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()
|
||||
&& island.structurePlan25().orElseThrow().protects(pos)) {
|
||||
callback.setReturnValue(false); return;
|
||||
}
|
||||
if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island)
|
||||
&& !fr.koka.sanctuary.expansion.ExpansionStructures23.isPlacing()) {
|
||||
var ocean = fr.koka.sanctuary.expansion.ExpansionOcean23.column(island, pos.getX(), pos.getZ());
|
||||
@@ -60,11 +74,37 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generator.rootGeneration() == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.isPlacing()) {
|
||||
if (!fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.mayWrite(pos)) callback.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
if (generator.rootGeneration() == 25 && pos.getY() >= 364
|
||||
&& (state.is(net.minecraft.tags.BlockTags.LOGS) || state.is(net.minecraft.tags.BlockTags.LEAVES))) {
|
||||
callback.setReturnValue(false); return;
|
||||
}
|
||||
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
if (generator.rootGeneration() == 25 && !generator.protectsCityDecoration(pos)
|
||||
&& region.getBlockState(pos).isAir()) {
|
||||
var island = generator.islandAt(pos.getX(), pos.getZ());
|
||||
if (island != null && island.origin()) {
|
||||
var terrain = generator.starterContext();
|
||||
if (fr.koka.sanctuary.worldgen.Terrain25Runtime.allowsWetlandPlant(
|
||||
terrain.generator(), terrain.random(), pos, state)) {
|
||||
// Native tree decorators continue hanging vines even after a protected
|
||||
// head/support was refused. Admit only plants that survive on real blocks.
|
||||
if (!state.canSurvive(region, pos)
|
||||
|| !fr.koka.sanctuary.worldgen.Terrain25WetlandPlantLight.lightSafe(region, pos, state)) {
|
||||
generator.rejectedDecorationWrite();
|
||||
callback.setReturnValue(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))
|
||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& generator.protectsCityDecoration(pos)) return;
|
||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||
|
||||
@@ -27,9 +27,9 @@ public enum IslandCapacity {
|
||||
public static IslandCapacity forGenerator(ChunkGenerator generator) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise)) return null;
|
||||
var settings = noise.generatorSettings();
|
||||
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))) return FIVE;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))) return TEN;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"))) return TWENTY;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_5"))) return FIVE;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_10"))) return TEN;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20")) || settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_25_20"))) return TWENTY;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_100_SETTINGS)) return HUNDRED;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,21 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
public static final MapCodec<SanctuaryBiomeSource> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 24)));
|
||||
public static final MapCodec<SanctuaryBiomeSource> TERRAIN25_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 25)));
|
||||
|
||||
|
||||
private volatile Terrain25Shape.Plan terrain25Plan;
|
||||
private volatile Terrain25Wetlands.Lookup terrain25Wetlands;
|
||||
public void setTerrain25Plan(Terrain25Shape.Plan plan) {
|
||||
if (generation != 25) throw new IllegalStateException("Terrain25 plan on historical biome source");
|
||||
terrain25Plan = java.util.Objects.requireNonNull(plan);
|
||||
}
|
||||
public void setTerrain25Wetlands(Terrain25Wetlands.Lookup wetlands) {
|
||||
if (generation != 25) throw new IllegalStateException("Terrain25 wetlands on historical biome source");
|
||||
terrain25Wetlands = java.util.Objects.requireNonNull(wetlands);
|
||||
}
|
||||
private final List<Holder<Biome>> allBiomes;
|
||||
private final PopulationIslandBiomeSource population;
|
||||
private final boolean cities;
|
||||
@@ -81,7 +95,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
this.generation = generation;
|
||||
this.cities = ExpansionIsland.spatialGeneration(generation);
|
||||
allBiomes = List.copyOf(biomes);
|
||||
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
|
||||
if ((generation == 23 || generation == 24 || generation == 25)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
|
||||
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
|
||||
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
|
||||
cityBiome = cities ? required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
|
||||
@@ -95,14 +109,19 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
}
|
||||
|
||||
/** A world or delegate gets its own activation snapshot; registered holders remain immutable. */
|
||||
public SanctuaryBiomeSource copy() { return new SanctuaryBiomeSource(allBiomes, generation); }
|
||||
public SanctuaryBiomeSource copy() {
|
||||
var copy = new SanctuaryBiomeSource(allBiomes, generation);
|
||||
copy.terrain25Plan = terrain25Plan;
|
||||
copy.terrain25Wetlands = terrain25Wetlands;
|
||||
return copy;
|
||||
}
|
||||
public boolean citiesEnabled() { return cities; }
|
||||
public int generation() { return generation; }
|
||||
public void setCityPlans(java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> plans) {
|
||||
cityPlans = java.util.Map.copyOf(plans);
|
||||
}
|
||||
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 25 ? TERRAIN25_CODEC : generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
|
||||
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
BiomeResolver woodlands = population.createResolver(sampler);
|
||||
@@ -122,6 +141,21 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
// Reuse the historical selector itself: its noise, thresholds, order and child
|
||||
// climate choices stay exact rather than being approximated by another climate field.
|
||||
Holder<Biome> laboratory = super.resolve(snapshot, x, y, z);
|
||||
if (generation == 25 && y >= 0 && y < 384) for (var island : snapshot) {
|
||||
if (!island.origin() || !island.contains(x, z)) continue;
|
||||
var shape = terrain25Plan;
|
||||
var wetlands = terrain25Wetlands;
|
||||
if (wetlands != null && wetlands.contains(x, y, z)) return required(Terrain25Biomes.key("swamp"));
|
||||
var chamber = shape == null ? null : shape.chamberAt(x, y, z);
|
||||
if (chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM)
|
||||
return required(Terrain25Biomes.MUSHROOM_REFUGE);
|
||||
if (laboratory.is(Biomes.LUSH_CAVES)) return required(Terrain25Biomes.key("lush_caves"));
|
||||
if (laboratory.is(Biomes.DRIPSTONE_CAVES)) return required(Terrain25Biomes.key("dripstone_caves"));
|
||||
if (laboratory.is(Biomes.SULFUR_CAVES)) return required(Terrain25Biomes.key("sulfur_depths"));
|
||||
var natural = woodlands.getNoiseBiome(quartX, quartY, quartZ);
|
||||
String name = natural.unwrapKey().orElseThrow().identifier().getPath();
|
||||
return required(Terrain25Biomes.key(name.substring("population_".length())));
|
||||
}
|
||||
if (y < 0 || y >= 384 || laboratory.is(Biomes.LUSH_CAVES)
|
||||
|| laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory;
|
||||
for (ExpansionIsland island : snapshot) {
|
||||
|
||||
+75
-46
@@ -80,19 +80,29 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 24, experimental)));
|
||||
public static final MapCodec<SanctuaryChunkGenerator> TERRAIN25_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.TERRAIN25_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 25, experimental)));
|
||||
|
||||
|
||||
private volatile fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24 = fr.koka.sanctuary.worldgen.aerial.AerialPlan24.EMPTY;
|
||||
public fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24() { return aerialPlan24; }
|
||||
public void setAerialPlan24(fr.koka.sanctuary.worldgen.aerial.AerialPlan24 plan) {
|
||||
if (generation != 24) throw new IllegalStateException("Aerial plans require generation24");
|
||||
if (generation != 24 && generation != 25) throw new IllegalStateException("Aerial plans require generation24");
|
||||
aerialPlan24 = java.util.Objects.requireNonNull(plan);
|
||||
}
|
||||
/** Plan root secrets before certifying aerial shore viewpoints, without requesting chunks. */
|
||||
public void prepareAerialSampling24(ServerLevel level) {
|
||||
if (generation != 24) throw new IllegalStateException("Aerial sampling requires generation24");
|
||||
prepareAerialSampling24(level, level.getSeed());
|
||||
}
|
||||
/** Prepare this generator only; diagnostics may supply a seed without attaching it to the level. */
|
||||
public void prepareAerialSampling24(ServerLevel level, long seed) {
|
||||
if (generation != 24 && generation != 25) throw new IllegalStateException("Aerial sampling requires generation24");
|
||||
heightAccessor = level;
|
||||
cityStructuresEnabled = level.structureManager().shouldGenerateStructures();
|
||||
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), level.getSeed(), 24));
|
||||
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), seed, generation));
|
||||
super.initialize(level, root);
|
||||
context(root.getFirst());
|
||||
// Same order as initialize. Prepared plans are reused after the manifest is persisted.
|
||||
@@ -165,12 +175,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||
super(source, initialDiameter, true);
|
||||
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && experimental;
|
||||
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && experimental;
|
||||
this.generation = generation;
|
||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||
}
|
||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 25 ? TERRAIN25_CODEC : generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||
}
|
||||
@@ -181,15 +191,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
if (generation == 24) for (var site : aerialPlan24.sites()) {
|
||||
if (generation == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(this).intersects(
|
||||
new net.minecraft.world.level.levelgen.structure.BoundingBox(minX, minY, minZ, maxX, maxY, maxZ))) return true;
|
||||
if (generation == 24 || generation == 25) for (var site : aerialPlan24.sites()) {
|
||||
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|
||||
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
|
||||
}
|
||||
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public IslandCapacity capacity() {
|
||||
return switch (initialDiameter()) {
|
||||
@@ -202,6 +214,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override public net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> structureBiome(
|
||||
net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> biome) {
|
||||
String name = biome.unwrapKey().orElseThrow().identifier().toString();
|
||||
if (name.startsWith("sanctuary:terrain25_")) {
|
||||
String path = name.substring("sanctuary:terrain25_".length());
|
||||
if (path.equals("swamp")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.SWAMP);
|
||||
if (path.equals("mushroom_refuge")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.MUSHROOM_FIELDS);
|
||||
if (path.equals("lush_caves")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.LUSH_CAVES);
|
||||
if (path.equals("dripstone_caves")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.DRIPSTONE_CAVES);
|
||||
if (path.equals("sulfur_depths")) return registries.lookupOrThrow(Registries.BIOME).getOrThrow(net.minecraft.world.level.biome.Biomes.SULFUR_CAVES);
|
||||
name = "sanctuary:population_" + path;
|
||||
}
|
||||
String equivalent = switch (name) {
|
||||
case "sanctuary:population_oak_forest" -> "forest";
|
||||
case "sanctuary:population_birch_forest" -> "birch_forest";
|
||||
@@ -224,16 +245,17 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
// Publish one root context before any generation workers use it.
|
||||
context(islands.getFirst());
|
||||
prepareCities(islands);
|
||||
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
if (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareMineral23(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
prepareTransit(islands);
|
||||
if (generation == 25) fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.prepare(level);
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
prepareCities(islands);
|
||||
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
if (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareMineral23(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
@@ -248,7 +270,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
return;
|
||||
}
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && !cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && !cityStructuresEnabled) {
|
||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||
activeCityPlans = java.util.Map.of();
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
@@ -264,7 +286,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
cityProtection15 = protection.build();
|
||||
}
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
for (var island : islands) {
|
||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||
@@ -279,13 +301,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(published);
|
||||
}
|
||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
var plan = (generation == 23 || generation == 24)
|
||||
var plan = (generation == 23 || generation == 24 || generation == 25)
|
||||
? fr.koka.sanctuary.worldgen.ruins.RuinsPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
ruinsPlan17 = plan;
|
||||
@@ -294,16 +316,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private void prepareHeritage(List<ExpansionIsland> islands) {
|
||||
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
|
||||
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = (generation == 23 || generation == 24)
|
||||
var plan = (generation == 23 || generation == 24 || generation == 25)
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: generation == 22
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
@@ -328,7 +350,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||
}
|
||||
private void prepareTransit(List<ExpansionIsland> islands) {
|
||||
if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
|
||||
if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||
@@ -339,7 +361,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
int directColumns = 0;
|
||||
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
|
||||
if ((generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.isEmpty()) {
|
||||
directColumns = sampled.columnsUsed();
|
||||
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
@@ -350,12 +372,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
|
||||
}
|
||||
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
|
||||
return (generation == 22 || (generation == 23 || generation == 24)) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
return (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
|| (generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
}
|
||||
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
|
||||
if ((generation != 22 && (generation != 23 && generation != 24)) || !cityStructuresEnabled || undergroundHallPrepared22) return;
|
||||
if ((generation != 22 && (generation != 23 && generation != 24 && generation != 25)) || !cityStructuresEnabled || undergroundHallPrepared22) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
|
||||
@@ -372,7 +394,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
|
||||
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
|
||||
};
|
||||
undergroundHall22 = (generation == 23 || generation == 24)
|
||||
undergroundHall22 = (generation == 23 || generation == 24 || generation == 25)
|
||||
? fr.koka.sanctuary.worldgen.city.LostCityUnderground23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain)
|
||||
: fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain);
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
@@ -382,7 +404,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private void prepareMineral23(List<ExpansionIsland> islands) {
|
||||
if ((generation != 23 && generation != 24) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
|
||||
if ((generation != 23 && generation != 24 && generation != 25) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
|
||||
@@ -420,13 +442,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityPlan lostCityPlan(ExpansionIsland island) {
|
||||
if ((generation == 23 || generation == 24) && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||
if ((generation == 23 || generation == 24 || generation == 25) && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||
if (!ExpansionIsland.spatialGeneration(generation) || island.generation() != generation || (!island.origin() && !island.climate().equals("lost_city")))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||
long start = System.nanoTime();
|
||||
var terrain = context(island);
|
||||
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
|
||||
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = generation >= 22
|
||||
@@ -531,13 +553,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (generation == 24 && aerialPlan24.protects(pos)) return true;
|
||||
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 25 && fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25.plan(this).protects(pos)) return true;
|
||||
if ((generation == 24 || generation == 25) && aerialPlan24.protects(pos)) return true;
|
||||
if ((generation == 23 || generation == 24 || generation == 25) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation != 14) return false;
|
||||
var island = islandAt(pos.getX(), pos.getZ());
|
||||
@@ -559,20 +582,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
lostCityPlan(island);
|
||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
}
|
||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return List.of();
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return List.of();
|
||||
lostCityPlan(island);
|
||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return null;
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24 && generation != 25)) return null;
|
||||
lostCityPlan(island);
|
||||
return cityReservations16.get(cityIdentity(island));
|
||||
}
|
||||
@@ -582,16 +605,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override public com.mojang.datafixers.util.Pair<net.minecraft.core.BlockPos, net.minecraft.core.Holder<net.minecraft.world.level.levelgen.structure.Structure>> findNearestMapStructure(
|
||||
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
|
||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if ((generation == 23 || generation == 24)) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 23 || generation == 24 || generation == 25)) return fr.koka.sanctuary.expansion.ExpansionStructures23.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (generation == 22 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
||||
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY))
|
||||
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
|
||||
if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
|
||||
return fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||
}
|
||||
@@ -604,10 +627,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
if (registries == null) throw new IllegalStateException("Sanctuary used before world attachment");
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.resources.ResourceKey.create(Registries.NOISE_SETTINGS,
|
||||
SanctuaryMod.id("unified_" + capacity().players())));
|
||||
SanctuaryMod.id((generation == 25 ? "unified_25_" : "unified_") + capacity().players())));
|
||||
var delegate = new NoiseBasedChunkGenerator(source(), settings);
|
||||
var random = RandomState.create(registries.lookupOrThrow(Registries.NOISE), island.seed(), settings.value());
|
||||
originContext = new Context(delegate, random);
|
||||
if (generation == 25) {
|
||||
Terrain25Integration.bind(delegate, this);
|
||||
source().setTerrain25Plan(Terrain25Runtime.view(delegate, random).plan());
|
||||
source().setTerrain25Wetlands((x, y, z) -> Terrain25Runtime.wetlandAt(delegate, random, x, y, z));
|
||||
}
|
||||
}
|
||||
return originContext;
|
||||
}
|
||||
@@ -620,7 +648,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && cityStructuresEnabled) {
|
||||
if (generation == 25 && island.origin()) Terrain25Integration.apply(this, chunk);
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24 || generation == 25)) && cityStructuresEnabled) {
|
||||
var reserved = cityReservations16.get(cityIdentity(island));
|
||||
if (reserved != null) {
|
||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
|
||||
+20
@@ -33,6 +33,19 @@ public final class SanctuaryDecorationProtection {
|
||||
for (var cell : region.lava().cells()) result.cube(cell.x(), cell.y(), cell.z(), 2);
|
||||
for (var support : region.lava().supports()) result.cube(support.x(), support.y(), support.z(), 2);
|
||||
}
|
||||
if (Terrain25Runtime.enabled(binding.generator())) {
|
||||
for (var lake : Terrain25Runtime.admittedLakes(binding.generator(), binding.random())) {
|
||||
int rx = (int) Math.ceil(lake.rx()), rz = (int) Math.ceil(lake.rz());
|
||||
result.box((int) Math.floor(lake.x()) - rx - 2, lake.floorY() - 3, (int) Math.floor(lake.z()) - rz - 2,
|
||||
(int) Math.ceil(lake.x()) + rx + 2, lake.fluidY() + 2, (int) Math.ceil(lake.z()) + rz + 2);
|
||||
}
|
||||
var capacity = IslandCapacity.forGenerator(binding.generator());
|
||||
var lo = capacity.regionAt(result.minX - 80, result.minZ - 80);
|
||||
var hi = capacity.regionAt(result.minX + 95, result.minZ + 95);
|
||||
for (int rx = lo.x(); rx <= hi.x(); rx++) for (int rz = lo.z(); rz <= hi.z(); rz++)
|
||||
for (var fall : Terrain25Runtime.falls(binding.generator(), binding.random(), new IslandCapacity.Region(rx, rz)))
|
||||
result.fall25(fall);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public boolean protects(BlockPos pos) {
|
||||
@@ -41,6 +54,13 @@ public final class SanctuaryDecorationProtection {
|
||||
&& mask.get((x * 16 + z) * 384 + y);
|
||||
}
|
||||
public int protectedBlocks() { return mask.cardinality(); }
|
||||
private void fall25(Terrain25Runtime.Fall fall) {
|
||||
for (int x = Math.max(minX, Terrain25FallProtection.minX(fall)); x <= Math.min(minX + 15, Terrain25FallProtection.maxX(fall)); x++)
|
||||
for (int z = Math.max(minZ, Terrain25FallProtection.minZ(fall)); z <= Math.min(minZ + 15, Terrain25FallProtection.maxZ(fall)); z++) {
|
||||
int low = Terrain25FallProtection.lowestY(fall, x, z);
|
||||
if (low <= Terrain25FallProtection.highestY(fall)) box(x, low, z, x, Terrain25FallProtection.highestY(fall), z);
|
||||
}
|
||||
}
|
||||
private void flow(PopulationHydrology.FlowBounds b, int pad) {
|
||||
box(b.minX() - pad, b.minY() - pad, b.minZ() - pad,
|
||||
b.maxX() + pad, b.maxY() + pad, b.maxZ() + pad);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
public final class Terrain25Biomes {
|
||||
public static final List<String> NAMES = List.of("oak_forest","birch_forest","clearing","dry_woodland","rocky_heath",
|
||||
"dark_grove","bamboo_grove","sulfur_depths","lush_caves","dripstone_caves","mushroom_refuge","swamp");
|
||||
public static final ResourceKey<Biome> DARK_GROVE = key("dark_grove"), BAMBOO_GROVE = key("bamboo_grove"), MUSHROOM_REFUGE = key("mushroom_refuge"), SWAMP = key("swamp");
|
||||
public static ResourceKey<Biome> key(String name) { return ResourceKey.create(Registries.BIOME,SanctuaryMod.id("terrain25_"+name)); }
|
||||
private Terrain25Biomes() {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Five-block mineral rind sampled against the intact rock, not against a later excavated room. */
|
||||
public final class Terrain25Coating {
|
||||
public static final int THICKNESS = 5;
|
||||
public static final int MIN_Y = 225;
|
||||
@FunctionalInterface public interface Solid { boolean at(int x, int y, int z); }
|
||||
private Terrain25Coating() {}
|
||||
|
||||
public static boolean calcite(int x, int y, int z, double lift, Solid intact) {
|
||||
if (y <= MIN_Y || lift <= 26) return false;
|
||||
// Retain the existing lower bands, but never extend them through a mountain's core.
|
||||
if (y <= 280 && (Math.floorMod(y, 11) < 3 || Math.floorMod(y, 11) > 9)) return false;
|
||||
return !intact.at(x, y + THICKNESS, z) || !intact.at(x, y - THICKNESS, z)
|
||||
|| !intact.at(x - THICKNESS, y, z) || !intact.at(x + THICKNESS, y, z)
|
||||
|| !intact.at(x, y, z - THICKNESS) || !intact.at(x, y, z + THICKNESS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Exact exclusions of the alpha.25 natural field, before its additional cave excavation. */
|
||||
public final class Terrain25ColumnBounds {
|
||||
public static final int LOWEST_POSSIBLE_ROCK = 24;
|
||||
private Terrain25ColumnBounds() {}
|
||||
|
||||
public static int highestPossibleRock(double lift) {
|
||||
// PopulationIslandShape clamps incoming terrain to <=1. Its upper erosion alone is
|
||||
// >=1 at mappedY>=304: localY>=240 makes 2*smoothstep((localY-224)/32)>=1.
|
||||
// Every other erosion term is nonnegative. Alpha.25's final fade also subtracts
|
||||
// >=1 at realY>=358. Neither interval can contain strictly positive density.
|
||||
int result=Math.min(357,(int)Math.ceil(150+154*(1+lift/95))-1);
|
||||
// Preserve the exact floor operation used by Plan.naturalY, including floating-point
|
||||
// boundary rounding. These corrections cost at most a couple of integer probes.
|
||||
while(mappedY(result,lift)>=304)result--;
|
||||
while(result<357&&mappedY(result+1,lift)<304)result++;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int mappedY(int y,double lift) {
|
||||
return y<=150?y:(int)Math.floor(150+(y-150)/(1+lift/95));
|
||||
}
|
||||
|
||||
/* At y<=23, mappedY=y. The lowest allowed underside parameter is
|
||||
-24-16-8-4=-52. Thus lower>=(84-y)/72, and 2*lower^4>1 for y<=23.
|
||||
Even terrain=1 cannot survive, regardless of the other nonnegative erosions. */
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** Alpha.25 relief and finite cave networks; earlier density codecs remain unchanged. */
|
||||
public record Terrain25Density(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
public static final MapCodec<Terrain25Density> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(Terrain25Density::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(Terrain25Density::distortion),
|
||||
DensityFunction.CODEC.fieldOf("sculpt").forGetter(Terrain25Density::sculpt),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(Terrain25Density::detail),
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(Terrain25Density::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(Terrain25Density::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(Terrain25Density::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(Terrain25Density::capacity)
|
||||
).apply(instance, Terrain25Density::new));
|
||||
|
||||
public interface View extends DensitySampler {
|
||||
Terrain25Shape.Plan plan();
|
||||
float natural(SamplerContext context, int x, int y, int z);
|
||||
}
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
long seed = context.createEndIslandRandom().nextLong();
|
||||
var proposed = Terrain25Shape.create(seed, capacity.radius());
|
||||
var columnLifts = ThreadLocal.withInitial(() -> new java.util.LinkedHashMap<Long, Double>(256, .75f, true) {
|
||||
@Override protected boolean removeEldestEntry(java.util.Map.Entry<Long, Double> eldest) { return size() > 4096; }
|
||||
});
|
||||
DensitySampler intact = new DensitySampler() {
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y > Terrain25Shape.MAX_TERRAIN_Y) return -1;
|
||||
long columnKey = ((long)x << 32) ^ (z & 0xffffffffL);
|
||||
double lift = columnLifts.get().computeIfAbsent(columnKey, ignored -> proposed.lift(x,z));
|
||||
int mappedY = proposed.naturalY(y, lift), localY = mappedY - 64;
|
||||
float result = PopulationIslandShape.raisedDensity(x, mappedY, z, raw.sampleValue(sample, x, localY, z),
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
// A broad fade rounds each pinnacle before the immutable build-height margin.
|
||||
return (float) Math.max(-1, result - Math.max(0, y - 348) / 10.0);
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample,DensityBuffer buffer,DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample,buffer,volume,this);
|
||||
}
|
||||
};
|
||||
var plan = Terrain25Shape.fit(proposed,(x,y,z)->intact.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z));
|
||||
return new View() {
|
||||
@Override public Terrain25Shape.Plan plan() { return plan; }
|
||||
@Override public float natural(SamplerContext sample,int x,int y,int z) { return intact.sampleValue(sample,x,y,z); }
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
float natural = natural(sample, x, y, z);
|
||||
if (natural <= 0) return natural;
|
||||
double cave = plan.excavation(x, y, z);
|
||||
// Short, slanting fissures; their axes drift with altitude and close at both ends.
|
||||
double fracture = -1;
|
||||
boolean retainBasin = plan.retainingBasin(x,y,z) != null || plan.dryFloor(x,y,z) != null;
|
||||
if (!retainBasin && y > 72 && y < 224 && Math.hypot(x, z) > 72) {
|
||||
double band = Math.sin((y - 72) / 152.0 * Math.PI);
|
||||
double a = warp.sampleValue(sample, x + (y - 140) / 2, y, z - (y - 140) / 3);
|
||||
double b = fractureDetail.sampleValue(sample, x, y, z);
|
||||
fracture = (.025 - Math.abs(a + b * .16)) * 8 * band - (1 - band) * .22;
|
||||
}
|
||||
return (float) Math.min(natural, -Math.max(cave, fracture));
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new Terrain25Density(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<Terrain25Density> codec() { return CODEC; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Water keeps its bed, source and outlet without reserving the dry rock under the entire channel. */
|
||||
public final class Terrain25FallProtection {
|
||||
private static final int BANK_MARGIN = 2;
|
||||
// Seven blocks of horizontal water spread plus the receiving column at the lip.
|
||||
private static final int OUTLET_MARGIN = 8;
|
||||
private Terrain25FallProtection() {}
|
||||
|
||||
/** First protected block in this column, or MAX_VALUE when the column is outside the water envelope. */
|
||||
public static int lowestY(Terrain25Runtime.Fall fall, int x, int z) {
|
||||
if (near(fall, x, z, .8, OUTLET_MARGIN)) return 0;
|
||||
if (near(fall, x, z, 0, BANK_MARGIN)
|
||||
|| Math.abs(x - fall.startX()) <= 3 && Math.abs(z - fall.startZ()) <= 3)
|
||||
return Math.max(0, fall.waterY() - 5);
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
public static int highestY(Terrain25Runtime.Fall fall) { return fall.waterY() + 13; }
|
||||
public static int minX(Terrain25Runtime.Fall fall) { return fall.minX() - OUTLET_MARGIN; }
|
||||
public static int maxX(Terrain25Runtime.Fall fall) { return fall.maxX() + OUTLET_MARGIN; }
|
||||
public static int minZ(Terrain25Runtime.Fall fall) { return fall.minZ() - OUTLET_MARGIN; }
|
||||
public static int maxZ(Terrain25Runtime.Fall fall) { return fall.maxZ() + OUTLET_MARGIN; }
|
||||
|
||||
/** Shared by admission and the per-chunk decoration mask. The query is an inclusive block volume. */
|
||||
public static boolean intersects(Terrain25Runtime.Fall fall, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
if (maxY < 0 || minY > highestY(fall) || minX > maxX(fall) || maxX < minX(fall)
|
||||
|| minZ > maxZ(fall) || maxZ < minZ(fall)) return false;
|
||||
for (int x = Math.max(minX, minX(fall)); x <= Math.min(maxX, maxX(fall)); x++)
|
||||
for (int z = Math.max(minZ, minZ(fall)); z <= Math.min(maxZ, maxZ(fall)); z++)
|
||||
if (lowestY(fall, x, z) <= maxY) return true;
|
||||
return false;
|
||||
}
|
||||
private static boolean near(Terrain25Runtime.Fall fall, int x, int z, double from, int margin) {
|
||||
double t = Math.clamp(fall.progress(x, z), from, 1);
|
||||
double cx = fall.startX() + (fall.endX() - fall.startX()) * t;
|
||||
double cz = fall.startZ() + (fall.endZ() - fall.startZ()) * t;
|
||||
double radius = 2.5 + (fall.width() * .5 - 2.5) * t + margin;
|
||||
double dx = x - cx, dz = z - cz;
|
||||
return dx * dx + dz * dz <= radius * radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.data.worldgen.features.VegetationFeatures;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VegetationBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
|
||||
/** Alpha.10: sheltered vanilla mushroom trees and dark oak on natural lower floors,
|
||||
* plus one globally selected remarkable tree.
|
||||
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
|
||||
public final class Terrain25GrovesFeature implements Feature {
|
||||
public static final MapCodec<Terrain25GrovesFeature> CODEC = MapCodec.unit(Terrain25GrovesFeature::new);
|
||||
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
|
||||
public RemarkableTree { base = base.immutable(); }
|
||||
}
|
||||
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
|
||||
private record Candidate(int x, int z, long rank) {}
|
||||
private interface NaturalSampler extends WoodlandGroveGeometry.Sampler {
|
||||
int naturalTop(int x,int z);
|
||||
}
|
||||
@FunctionalInterface
|
||||
private interface Protection {
|
||||
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
|
||||
}
|
||||
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
|
||||
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
|
||||
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
|
||||
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
|
||||
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
|
||||
|
||||
@Override public MapCodec<Terrain25GrovesFeature> codec() { return CODEC; }
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
|
||||
var binding = PopulationGenerationContext.of(level);
|
||||
return binding == null ? Optional.empty() : remarkablePlan(binding.generator(), binding.random());
|
||||
}
|
||||
|
||||
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
|
||||
if (!PopulationHydrologyRuntime.enabled(noise)) return Optional.empty();
|
||||
return REMARKABLE.computeIfAbsent(state, ignored -> {
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
Terrain25Runtime.protects(noise, state, base, radius, height, rootDepth);
|
||||
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
// One global candidate stays in the starter centre; ordinary groves use the full capacity.
|
||||
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
|
||||
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
|
||||
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
|
||||
}
|
||||
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
|
||||
for (Candidate candidate : candidates) {
|
||||
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
|
||||
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
|
||||
if (base.getY() + species.height() >= 364 || blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
|
||||
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
|
||||
species.width(), species.radius(), species.height())) continue;
|
||||
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
var binding = PopulationGenerationContext.of(generator, level.getLevel());
|
||||
if (binding == null || !Terrain25Runtime.enabled(binding.generator())) return false;
|
||||
var noise = binding.generator();
|
||||
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
|
||||
if (!PopulationHydrologyRuntime.capacity(noise).areaExists(minX >> 4, minZ >> 4)) return false;
|
||||
var state = binding.random();
|
||||
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
|
||||
NaturalSampler sampler = new NaturalSampler() {
|
||||
@Override public float sample(int x,int y,int z) {return state.sampleBlockValueUncached(density,x,y,z);}
|
||||
@Override public int naturalTop(int x,int z) {return Terrain25Runtime.naturalTop(noise,state,x,z);}
|
||||
};
|
||||
Protection protection = (base, radius, height, rootDepth) ->
|
||||
Terrain25Runtime.protects(noise, state, base, radius, height, rootDepth);
|
||||
boolean changed = false;
|
||||
int[] heights = new int[256];
|
||||
Arrays.fill(heights, Integer.MIN_VALUE);
|
||||
var remarkable = remarkablePlan(noise, state);
|
||||
if (remarkable.isPresent()) {
|
||||
var tree = remarkable.get();
|
||||
BlockPos base = tree.base();
|
||||
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
|
||||
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
|
||||
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
|
||||
// give mushroom trees first choice. This changes priority, never attempt budgets
|
||||
// or natural fit requirements, and uses a separate deterministic chunk seed.
|
||||
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
|
||||
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
|
||||
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
|
||||
for (int bed = highest - 8; bed >= 40; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
|
||||
var biome = level.getBiome(base);
|
||||
boolean dark = biome.is(Terrain25Biomes.DARK_GROVE);
|
||||
boolean bamboo = biome.is(Terrain25Biomes.BAMBOO_GROVE);
|
||||
if (!dark && !bamboo) continue;
|
||||
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
|
||||
if (dark) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 8) == 0) {
|
||||
if (tree(level, generator, sampler, state.seed(), base,
|
||||
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
|
||||
} else if (Math.floorMod(placementSeed, 3) == 0
|
||||
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
|
||||
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
|
||||
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
|
||||
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
|
||||
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
|
||||
.value().place(level, generator, RandomSource.create(placementSeed), base);
|
||||
placed++;
|
||||
}
|
||||
if (placed >= 2) break;
|
||||
}
|
||||
}
|
||||
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
|
||||
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
|
||||
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
|
||||
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
|
||||
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
|
||||
boolean smallMushroomPlaced = false;
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
|
||||
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
|
||||
var biome = level.getBiome(plant);
|
||||
if (!biome.is(Terrain25Biomes.DARK_GROVE) && !biome.is(Terrain25Biomes.BAMBOO_GROVE)) continue;
|
||||
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
|
||||
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
|
||||
int soilKind = Math.floorMod(patch, 5);
|
||||
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
|
||||
setBlock(level, floor, soil);
|
||||
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
|
||||
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
|
||||
if (mushroom.canSurvive(level, plant)) {
|
||||
setBlock(level, plant, mushroom);
|
||||
smallMushroomPlaced = true;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
if (++floors >= 3) break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, NaturalSampler sampler,
|
||||
long seed, Protection protection, int minX, int minZ, int[] heights) {
|
||||
boolean changed = false;
|
||||
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
|
||||
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
|
||||
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
|
||||
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
|
||||
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
|
||||
for (int bed = highest - 8; bed >= 32; bed--) {
|
||||
BlockPos base = new BlockPos(x, bed + 1, z);
|
||||
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|
||||
|| !level.getBiome(base).is(Terrain25Biomes.DARK_GROVE)) continue;
|
||||
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
|
||||
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
|
||||
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
|
||||
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
|
||||
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
|
||||
int radius = mushroom.foliageRadius();
|
||||
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
|
||||
BlockState originalFloor = level.getBlockState(base.below());
|
||||
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
|
||||
// Vanilla consumes its height draw first. Replaying the same random seed
|
||||
// gives precisely the height whose natural envelope was checked above.
|
||||
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
|
||||
if (!placed) setBlock(level, base.below(), originalFloor);
|
||||
changed |= placed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
|
||||
BlockPos base, Species species, Protection protection, long salt) {
|
||||
if (base.getY() + species.height() >= 364 || blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|
||||
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|
||||
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
BlockPos floor = base.offset(dx, -1, dz);
|
||||
if (!ground(level, floor)) return false;
|
||||
}
|
||||
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
|
||||
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
|
||||
}
|
||||
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
|
||||
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
|
||||
}
|
||||
|
||||
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
|
||||
for (int y = 0; y <= height; y++) {
|
||||
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
|
||||
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
|
||||
BlockState current = level.getBlockState(base.offset(dx, y, dz));
|
||||
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|
||||
|| current.getBlock() instanceof VegetationBlock)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean ground(WorldGenLevel level, BlockPos floor) {
|
||||
BlockState state = level.getBlockState(floor);
|
||||
if (state.is(Blocks.DEEPSLATE)) {
|
||||
// Deep humid ledges can support organic soil. Check every tree foot rather
|
||||
// than admitting this rock globally, including for the remarkable tree.
|
||||
var biome = level.getBiome(floor.above());
|
||||
return biome.is(Terrain25Biomes.DARK_GROVE) || biome.is(Terrain25Biomes.BAMBOO_GROVE);
|
||||
}
|
||||
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|
||||
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|
||||
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
|
||||
}
|
||||
|
||||
private static int localTop(WorldGenLevel level, NaturalSampler sampler, Protection protection,
|
||||
int minX, int minZ, int x, int z, int[] heights) {
|
||||
int index = (x - minX) * 16 + z - minZ;
|
||||
if (heights[index] != Integer.MIN_VALUE) return heights[index];
|
||||
// In an untouched column the heightmap bounds the original surface cheaply.
|
||||
// A protected water/lava column may have been carved: recover its raw top
|
||||
// without depending on the current hydrology plan's representation.
|
||||
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
|
||||
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
|
||||
ceiling = level.getMaxY();
|
||||
// New caverns and fissures only subtract from the intact field. Reusing its exact
|
||||
// cached top bounds the same final-density scan without repeating the empty sky.
|
||||
ceiling = Math.min(ceiling,sampler.naturalTop(x,z));
|
||||
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
|
||||
}
|
||||
|
||||
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
|
||||
BlockPos base, int radius, int height) {
|
||||
if (blocked(protection, base, radius + 1, height + 1, 4)
|
||||
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
|
||||
for (int depth = 1; depth <= 3; depth++) {
|
||||
BlockPos floor = base.below(depth);
|
||||
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
|
||||
}
|
||||
// A complete conservative column around the cap handles both brown umbrellas
|
||||
// and the lower rounded red cap. Neither natural rock nor existing logs move.
|
||||
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
BlockPos p = base.offset(dx, y, dz);
|
||||
BlockState current = level.getBlockState(p);
|
||||
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|
||||
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
|
||||
}
|
||||
// Being below another surface is not enough: the same column needs an actual
|
||||
// original rock ceiling above the whole mushroom, not an open hillside.
|
||||
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
|
||||
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
|
||||
return blocked(protection, base, radius, height, 4);
|
||||
}
|
||||
|
||||
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
|
||||
return protection.intersects(base, radius, height, rootDepth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundPlan25;
|
||||
import fr.koka.sanctuary.worldgen.underground25.UndergroundStructures25;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
|
||||
/** Makes accepted local floors visible both to native piece projection and terrain generation. */
|
||||
public final class Terrain25Integration {
|
||||
private static final Map<NoiseBasedChunkGenerator, WeakReference<SanctuaryChunkGenerator>> HOSTS = new WeakHashMap<>();
|
||||
private Terrain25Integration() {}
|
||||
public static synchronized void bind(NoiseBasedChunkGenerator delegate, SanctuaryChunkGenerator host) {
|
||||
HOSTS.put(delegate, new WeakReference<>(host));
|
||||
}
|
||||
private static synchronized UndergroundPlan25 plan(NoiseBasedChunkGenerator delegate) {
|
||||
var reference = HOSTS.get(delegate);
|
||||
var host = reference == null ? null : reference.get();
|
||||
return host == null ? UndergroundPlan25.EMPTY : UndergroundStructures25.plan(host);
|
||||
}
|
||||
public static void column(NoiseBasedChunkGenerator delegate, int x, int z, NoiseColumn column) {
|
||||
var plan = plan(delegate);
|
||||
if (plan.starts().isEmpty()) return;
|
||||
for (var start : plan.starts()) for (var piece : start.start().getPieces())
|
||||
if (piece instanceof fr.koka.sanctuary.worldgen.underground25.UndergroundPiece25 extra) {
|
||||
var box = extra.getBoundingBox();
|
||||
if (x < box.minX() || x > box.maxX() || z < box.minZ() || z > box.maxZ()) continue;
|
||||
for (int y = box.minY(); y <= box.maxY(); y++) {
|
||||
var state = extra.terrainOverride(x, y, z);
|
||||
if (state != null) column.setBlock(y, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void apply(SanctuaryChunkGenerator host, ChunkAccess chunk) {
|
||||
var plan = UndergroundStructures25.plan(host);
|
||||
if (plan.starts().isEmpty()) return;
|
||||
var at = chunk.getPos();
|
||||
var box = new net.minecraft.world.level.levelgen.structure.BoundingBox(at.getMinBlockX(), 0, at.getMinBlockZ(), at.getMaxBlockX(), 383, at.getMaxBlockZ());
|
||||
if (!plan.intersects(box)) return;
|
||||
var pos = new BlockPos.MutableBlockPos();
|
||||
for (var start : plan.starts()) for (var piece : start.start().getPieces())
|
||||
if (piece instanceof fr.koka.sanctuary.worldgen.underground25.UndergroundPiece25 extra) {
|
||||
var bounds = extra.getBoundingBox();
|
||||
if (!bounds.intersects(box)) continue;
|
||||
for (int x = Math.max(at.getMinBlockX(), bounds.minX()); x <= Math.min(at.getMaxBlockX(), bounds.maxX()); x++)
|
||||
for (int z = Math.max(at.getMinBlockZ(), bounds.minZ()); z <= Math.min(at.getMaxBlockZ(), bounds.maxZ()); z++)
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
|
||||
var state = extra.terrainOverride(x, y, z);
|
||||
if (state != null) chunk.setBlockState(pos.set(x, y, z), state, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.features.TreeFeatures;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
|
||||
/** Chunk decoration owns initial occupants; normal chunk completion persists them once, like vanilla fauna. */
|
||||
public final class Terrain25RefugeFeature implements Feature {
|
||||
public static final MapCodec<Terrain25RefugeFeature> CODEC = MapCodec.unit(Terrain25RefugeFeature::new);
|
||||
@Override public MapCodec<Terrain25RefugeFeature> codec() { return CODEC; }
|
||||
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
|
||||
var binding=PopulationGenerationContext.of(generator,level.getLevel());
|
||||
if(binding==null || !Terrain25Runtime.enabled(binding.generator())) return false;
|
||||
int minX=(origin.getX()>>4)*16,minZ=(origin.getZ()>>4)*16;
|
||||
var plan=Terrain25Runtime.shape(binding.generator(),binding.random());
|
||||
boolean changed=Terrain25WetlandVegetation.place(level,binding.generator(),binding.random(),minX,minZ);
|
||||
for(var chamber:plan.chambers()) {
|
||||
if(chamber.kind()!=Terrain25Shape.Kind.MUSHROOM || minX>chamber.x()+chamber.rx() || minX+15<chamber.x()-chamber.rx()
|
||||
|| minZ>chamber.z()+chamber.rz() || minZ+15<chamber.z()-chamber.rz()) continue;
|
||||
for(int x=minX+5;x<=minX+10;x+=5) for(int z=minZ+5;z<=minZ+10;z+=5) {
|
||||
if(chamber.horizontal(x,z)>.52)continue;
|
||||
BlockPos base=new BlockPos(x,chamber.floorY()+1,z);
|
||||
if(!level.getBlockState(base).isAir() || !(level.getBlockState(base.below()).is(Blocks.MYCELIUM)
|
||||
|| level.getBlockState(base.below()).is(Blocks.MOSS_BLOCK) || level.getBlockState(base.below()).is(Blocks.PODZOL)))continue;
|
||||
if(Terrain25Runtime.protects(binding.generator(),binding.random(),base,4,13,1))continue;
|
||||
long seed=WoodlandGroveGeometry.seed(level.getSeed(),x,base.getY(),z,0x25A115L);
|
||||
if(Math.floorMod(seed,3)!=0)continue;
|
||||
var mushroom=level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(
|
||||
Math.floorMod(seed,2)==0?TreeFeatures.HUGE_BROWN_MUSHROOM:TreeFeatures.HUGE_RED_MUSHROOM).value();
|
||||
changed|=mushroom.place(level,generator,RandomSource.create(seed),base);
|
||||
}
|
||||
int cx=(int)Math.round(chamber.x()),cz=(int)Math.round(chamber.z());
|
||||
if((cx>>4)!=(minX>>4) || (cz>>4)!=(minZ>>4))continue;
|
||||
for(int i=0;i<3;i++) {
|
||||
BlockPos at=new BlockPos(cx+(i-1)*3,chamber.floorY()+1,cz);
|
||||
if(!level.getBlockState(at).isAir() || !level.getBlockState(at.above()).isAir()
|
||||
|| !level.getBlockState(at.below()).isSolid())continue;
|
||||
var cow=EntityTypes.MOOSHROOM.create(level.getLevel(),EntitySpawnReason.CHUNK_GENERATION);
|
||||
if(cow==null)continue;
|
||||
cow.snapTo(at.getX()+.5,at.getY(),at.getZ()+.5,0,0);
|
||||
cow.finalizeSpawn(level,level.getCurrentDifficultyAt(at),EntitySpawnReason.CHUNK_GENERATION,null);
|
||||
cow.setPersistenceRequired();
|
||||
cow.setUUID(UUID.nameUUIDFromBytes((level.getSeed()+":terrain25:mooshroom:"+chamber.id()+":"+i).getBytes(StandardCharsets.UTF_8)));
|
||||
level.addFreshEntityWithPassengers(cow);changed=true;
|
||||
}
|
||||
}
|
||||
// Preserve a strict vegetation margin even for the inherited vanilla tree features.
|
||||
for(int x=minX;x<minX+16;x++)for(int z=minZ;z<minZ+16;z++)for(int y=364;y<384;y++) {
|
||||
BlockPos at=new BlockPos(x,y,z);var block=level.getBlockState(at);
|
||||
if(block.is(BlockTags.LOGS) || block.is(BlockTags.LEAVES)) {level.setBlock(at,Blocks.AIR.defaultBlockState(),2);changed=true;}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Shared final terrain view. Planners read samplers only: never chunks or intercepted base columns. */
|
||||
public final class Terrain25Runtime {
|
||||
private static final Map<RandomState, World> WORLDS = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private record Column(int bottom, int top, double lift) {}
|
||||
private record UpperRock(long low, long middle, long high) {
|
||||
private static final int MIN_Y = Terrain25Coating.MIN_Y - Terrain25Coating.THICKNESS;
|
||||
private static final UpperRock EMPTY = new UpperRock(0,0,0);
|
||||
boolean solid(int y) {
|
||||
int bit=y-MIN_Y;
|
||||
if(bit<0 || bit>=192)return false;
|
||||
long word=bit<64?low:bit<128?middle:high;
|
||||
return (word & (1L << (bit&63))) != 0;
|
||||
}
|
||||
}
|
||||
private record MaterialColumn(Column geological, Terrain25Coating.Solid shell) {}
|
||||
public record Fall(long river, int startX, int startZ, int endX, int endZ, int waterY, int width) {
|
||||
public int minX() { return Math.min(startX, endX) - width; }
|
||||
public int maxX() { return Math.max(startX, endX) + width; }
|
||||
public int minZ() { return Math.min(startZ, endZ) - width; }
|
||||
public int maxZ() { return Math.max(startZ, endZ) + width; }
|
||||
public double progress(int x, int z) {
|
||||
double dx = endX - startX, dz = endZ - startZ;
|
||||
return ((x - startX) * dx + (z - startZ) * dz) / (dx * dx + dz * dz);
|
||||
}
|
||||
public boolean channel(int x, int z) {
|
||||
double t = progress(x, z);
|
||||
if (t < 0 || t > 1) return false;
|
||||
double dx = endX - startX, dz = endZ - startZ;
|
||||
double distance = Math.abs((x - startX) * dz - (z - startZ) * dx) / Math.hypot(dx, dz);
|
||||
return distance <= 2.5 + (width * .5 - 2.5) * t;
|
||||
}
|
||||
}
|
||||
private static final class World {
|
||||
final Terrain25Density.View density;
|
||||
final List<Terrain25Shape.Chamber> lakes;
|
||||
final Map<Long, Column> columns = Collections.synchronizedMap(new LinkedHashMap<>(1024, .75f, true) {
|
||||
@Override protected boolean removeEldestEntry(Map.Entry<Long, Column> eldest) { return size() > 32768; }
|
||||
});
|
||||
final Map<Long, UpperRock> upperRock = Collections.synchronizedMap(new LinkedHashMap<>(1024, .75f, true) {
|
||||
@Override protected boolean removeEldestEntry(Map.Entry<Long, UpperRock> eldest) { return size() > 32768; }
|
||||
});
|
||||
final PopulationPlanCache<IslandCapacity.Region, List<Fall>> falls = new PopulationPlanCache<>();
|
||||
final PopulationPlanCache<IslandCapacity.Region, Terrain25Wetlands.Plan> wetlands = new PopulationPlanCache<>();
|
||||
final PopulationPlanCache<Integer, Terrain25Wetlands.Plan> lowerWetlands = new PopulationPlanCache<>();
|
||||
World(Terrain25Density.View density, NoiseBasedChunkGenerator generator, RandomState state) {
|
||||
this.density = density;
|
||||
var proposed = density.plan().chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.LAKE || c.kind() == Terrain25Shape.Kind.LAVA).toList();
|
||||
List<Terrain25Shape.Chamber> accepted = new ArrayList<>();
|
||||
for (var candidate : proposed) if (accepted.stream().noneMatch(other -> Terrain25Shape.fluidConflict(candidate,other))
|
||||
&& retained(density,candidate) && legacyFluidSafe(generator,state,candidate)) accepted.add(candidate);
|
||||
lakes = List.copyOf(accepted);
|
||||
SanctuaryMod.LOGGER.info("Terrain25 seed {}: {} underground fluid basins accepted / {} proposed", state.seed(), lakes.size(), proposed.size());
|
||||
}
|
||||
Column column(int x, int z) {
|
||||
long key = key(x, z);
|
||||
Column found = columns.get(key);
|
||||
if (found != null) return found;
|
||||
int top = -1, bottom = -1;
|
||||
double lift=density.plan().lift(x,z);
|
||||
int ceiling=Terrain25ColumnBounds.highestPossibleRock(lift);
|
||||
for (int y = ceiling; y >= Terrain25ColumnBounds.LOWEST_POSSIBLE_ROCK; y--) if (density.natural(SamplerContext.EMPTY_UNCACHED, x, y, z) > 0) { top = y; break; }
|
||||
if (top >= 0) for (int y = Terrain25ColumnBounds.LOWEST_POSSIBLE_ROCK; y <= top; y++) if (density.natural(SamplerContext.EMPTY_UNCACHED, x, y, z) > 0) { bottom = y; break; }
|
||||
found = new Column(bottom, top, lift); columns.put(key, found); return found;
|
||||
}
|
||||
UpperRock upperRock(int x,int z) {
|
||||
long key=key(x,z);
|
||||
UpperRock cached=upperRock.get(key);
|
||||
if(cached!=null)return cached;
|
||||
Column column=column(x,z);
|
||||
if(column.top<UpperRock.MIN_Y)return UpperRock.EMPTY;
|
||||
long[] bits=new long[3];
|
||||
// At most139 natural-density samples per requested column. The same profile is
|
||||
// reused by adjacent blocks and neighbouring chunks; no per-block density stencil.
|
||||
for(int y=UpperRock.MIN_Y;y<=Math.min(358,column.top);y++)
|
||||
if(density.natural(SamplerContext.EMPTY_UNCACHED,x,y,z)>0) {
|
||||
int bit=y-UpperRock.MIN_Y;bits[bit>>>6]|=1L<<(bit&63);
|
||||
}
|
||||
UpperRock result=new UpperRock(bits[0],bits[1],bits[2]);upperRock.put(key,result);return result;
|
||||
}
|
||||
MaterialColumn materials(int x,int z) {
|
||||
Column column=column(x,z);
|
||||
if(column.lift<=26 || column.top<=Terrain25Coating.MIN_Y)return new MaterialColumn(column,null);
|
||||
UpperRock centre=upperRock(x,z),west=upperRock(x-5,z),east=upperRock(x+5,z),north=upperRock(x,z-5),south=upperRock(x,z+5);
|
||||
Terrain25Coating.Solid shell=(nx,y,nz)->(nx<x?west:nx>x?east:nz<z?north:nz>z?south:centre).solid(y);
|
||||
return new MaterialColumn(column,shell);
|
||||
}
|
||||
}
|
||||
private Terrain25Runtime() {}
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) {
|
||||
return generator.generatorSettings().is(SanctuaryMod.id("unified_25_5"))
|
||||
|| generator.generatorSettings().is(SanctuaryMod.id("unified_25_10"))
|
||||
|| generator.generatorSettings().is(SanctuaryMod.id("unified_25_20"));
|
||||
}
|
||||
public static Terrain25Density.View view(NoiseBasedChunkGenerator generator, RandomState state) {
|
||||
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
|
||||
if (sampler instanceof Terrain25Density.View view) return view;
|
||||
throw new IllegalStateException("Terrain25 settings must compile a Terrain25Density.View: " + sampler.getClass().getName());
|
||||
}
|
||||
private static World world(NoiseBasedChunkGenerator generator, RandomState state) {
|
||||
synchronized (WORLDS) { return WORLDS.computeIfAbsent(state, ignored -> new World(view(generator, state),generator,state)); }
|
||||
}
|
||||
public static Terrain25Shape.Plan shape(NoiseBasedChunkGenerator generator, RandomState state) { return world(generator, state).density.plan(); }
|
||||
/** Exact intact ceiling, already cached by the surface material pass; cave cuts never exceed it. */
|
||||
public static int naturalTop(NoiseBasedChunkGenerator generator, RandomState state, int x, int z) {
|
||||
return world(generator,state).column(x,z).top;
|
||||
}
|
||||
public static boolean isMushroomRefuge(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z) {
|
||||
var chamber = shape(generator, state).chamberAt(x, y, z);
|
||||
return chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM;
|
||||
}
|
||||
public static List<Terrain25Shape.Chamber> admittedLakes(NoiseBasedChunkGenerator generator, RandomState state) { return world(generator, state).lakes; }
|
||||
/** A real 3D biome attached to existing water and shore columns, independent of structure options. */
|
||||
public static boolean wetlandAt(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z) {
|
||||
var capacity=PopulationHydrologyRuntime.capacity(generator);
|
||||
return y>=0 && y<384 && Math.hypot(x,z)<capacity.terrainLimit()
|
||||
&& wetlandPlan(generator,state,capacity.regionAt(x,z)).contains(x,y,z);
|
||||
}
|
||||
public static Terrain25Wetlands.Plan wetlandPlan(NoiseBasedChunkGenerator generator, RandomState state, IslandCapacity.Region region) {
|
||||
var capacity=PopulationHydrologyRuntime.capacity(generator);
|
||||
double nx=Math.max(0,Math.max(region.minX(),-region.maxX())),nz=Math.max(0,Math.max(region.minZ(),-region.maxZ()));
|
||||
if(Math.hypot(nx,nz)>=capacity.terrainLimit())return Terrain25Wetlands.Plan.EMPTY;
|
||||
var world=world(generator,state);
|
||||
return world.wetlands.getOrCreate(region,()->{
|
||||
var hydrology=PopulationHydrologyRuntime.regionPlan(generator,state,region);
|
||||
// Only neighbouring immutable metadata is consulted. The current regional plan
|
||||
// never calls finalBlock or a biome resolver while it is being constructed.
|
||||
var neighbours=PopulationHydrologyRuntime.plansIntersecting(generator,state,
|
||||
region.minX()-32,region.minZ()-32,region.maxX()+32,region.maxZ()+32);
|
||||
var outlets=new LinkedHashSet<Fall>();
|
||||
for(var neighbour:neighbours)outlets.addAll(falls(generator,state,neighbour.region()));
|
||||
var terrain=new Terrain25Wetlands.Terrain(){
|
||||
@Override public int top(int x,int z){
|
||||
if(Math.hypot(x,z)>=capacity.terrainLimit())return -1;
|
||||
for(int y=world.column(x,z).top;y>=24;y--)
|
||||
if(world.density.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0)return y;
|
||||
return -1;
|
||||
}
|
||||
@Override public boolean solid(int x,int y,int z){
|
||||
return world.density.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;
|
||||
}
|
||||
@Override public double lift(int x,int z){return world.density.plan().lift(x,z);}
|
||||
@Override public boolean excluded(int x,int minY,int maxY,int z){
|
||||
for(var fall:outlets)if(Terrain25FallProtection.intersects(fall,x,minY,z,x,maxY,z))return true;
|
||||
for(var basin:world.lakes)if(Math.abs(x-basin.x())<=basin.rx()+4 && Math.abs(z-basin.z())<=basin.rz()+4
|
||||
&& minY<=basin.fluidY()+4 && maxY>=basin.floorY()-4)return true;
|
||||
for(var neighbour:neighbours){
|
||||
for(var spring:neighbour.water().springs())if(intersects(spring.flowBounds(),x,minY,maxY,z))return true;
|
||||
for(var spill:neighbour.water().spills())if(intersects(spill.flowBounds(),x,minY,maxY,z))return true;
|
||||
for(var hot:neighbour.lava().flowBounds())if(intersects(hot,x,minY,maxY,z))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var surface=Terrain25Wetlands.create(state.seed(),hydrology.water().cells(),hydrology.water().features(),terrain);
|
||||
var lower=world.lowerWetlands.getOrCreate(0,()->createLowerWetlands(generator,state,world));
|
||||
var plan=Terrain25Wetlands.combine(surface,lower);
|
||||
SanctuaryMod.LOGGER.info("Terrain25 wetland seed {} region {},{}: {} sites, {} columns, {} candidates, refusals {}",
|
||||
state.seed(),region.x(),region.z(),plan.sites().size(),plan.columns().size(),plan.candidates(),plan.refusals());
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
private static Terrain25Wetlands.Plan createLowerWetlands(NoiseBasedChunkGenerator generator,RandomState state,World world) {
|
||||
// One seed-wide lower plan is shared by all regional lookups, including overlapping X/Z strata.
|
||||
// It reads density and already planned hydrology only, never a biome, chunk or finalBlock.
|
||||
var neighbours=new LinkedHashMap<IslandCapacity.Region,PopulationHydrologyRuntime.RegionPlan>();
|
||||
for(var lake:world.lakes)if(lake.kind()==Terrain25Shape.Kind.LAKE)
|
||||
for(var plan:PopulationHydrologyRuntime.plansIntersecting(generator,state,
|
||||
(int)(lake.x()-lake.rx()-32),(int)(lake.z()-lake.rz()-32),
|
||||
(int)(lake.x()+lake.rx()+32),(int)(lake.z()+lake.rz()+32)))neighbours.put(plan.region(),plan);
|
||||
var outlets=new LinkedHashSet<Fall>();
|
||||
for(var plan:neighbours.values())outlets.addAll(falls(generator,state,plan.region()));
|
||||
var terrain=new Terrain25Wetlands.Terrain(){
|
||||
@Override public int top(int x,int z){return world.column(x,z).top;}
|
||||
@Override public boolean solid(int x,int y,int z){return world.density.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
|
||||
@Override public double lift(int x,int z){return world.density.plan().lift(x,z);}
|
||||
@Override public boolean excluded(int x,int minY,int maxY,int z){
|
||||
for(var fall:outlets)if(Terrain25FallProtection.intersects(fall,x,minY,z,x,maxY,z))return true;
|
||||
for(var basin:world.lakes)if(basin.kind()==Terrain25Shape.Kind.LAVA&&basin.horizontal(x,z)<1.5
|
||||
&&minY<=basin.fluidY()+6&&maxY>=basin.floorY()-6)return true;
|
||||
for(var chamber:world.density.plan().chambers())if(chamber.kind()==Terrain25Shape.Kind.MUSHROOM
|
||||
&&chamber.horizontal(x,z)<1.1&&minY<=chamber.y()+chamber.ry()&&maxY>=chamber.floorY()-3)return true;
|
||||
for(var neighbour:neighbours.values()){
|
||||
for(var spring:neighbour.water().springs())if(intersects(spring.flowBounds(),x,minY,maxY,z))return true;
|
||||
for(var spill:neighbour.water().spills())if(intersects(spill.flowBounds(),x,minY,maxY,z))return true;
|
||||
for(var hot:neighbour.lava().flowBounds())if(intersects(hot,x,minY,maxY,z))return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return Terrain25Wetlands.createUnderground(state.seed(),world.lakes,terrain);
|
||||
}
|
||||
private static boolean intersects(PopulationHydrology.FlowBounds b,int x,int minY,int maxY,int z){
|
||||
return x>=b.minX()-2&&x<=b.maxX()+2&&z>=b.minZ()-2&&z<=b.maxZ()+2&&minY<=b.maxY()+2&&maxY>=b.minY()-2;
|
||||
}
|
||||
/** Stable soil replacement only; the wetland never fills air or creates a support platform. */
|
||||
public static BlockState shoreMaterial(Terrain25Wetlands.Column column,long seed,int y,BlockState original){
|
||||
if(column==null || y<column.floorY()-1 || y>column.floorY() || original.isAir()
|
||||
|| !original.getFluidState().isEmpty())return original;
|
||||
if(!(original.is(Blocks.STONE)||original.is(Blocks.DEEPSLATE)||original.is(Blocks.TUFF)
|
||||
|| original.is(Blocks.ANDESITE)||original.is(Blocks.GRANITE)||original.is(Blocks.DIORITE)
|
||||
|| original.is(Blocks.CALCITE)||original.is(Blocks.DRIPSTONE_BLOCK)||original.is(Blocks.BASALT)
|
||||
|| original.is(Blocks.SMOOTH_BASALT)||original.is(Blocks.BLACKSTONE)||original.is(Blocks.COBBLESTONE)
|
||||
|| original.is(Blocks.MOSSY_COBBLESTONE)||original.is(Blocks.PACKED_MUD)
|
||||
|| original.is(Blocks.DIRT)||original.is(Blocks.GRASS_BLOCK)
|
||||
|| original.is(Blocks.COARSE_DIRT)||original.is(Blocks.PODZOL)||original.is(Blocks.MYCELIUM)||original.is(Blocks.MOSS_BLOCK)
|
||||
|| original.is(Blocks.SAND)||original.is(Blocks.GRAVEL)||original.is(Blocks.CLAY)||original.is(Blocks.MUD)))return original;
|
||||
if(y<column.floorY())return (column.wet()?Blocks.CLAY:Blocks.DIRT).defaultBlockState();
|
||||
int patch=Math.floorMod(mix(seed^key(Math.floorDiv(column.x(),5),Math.floorDiv(column.z(),5))),4);
|
||||
return (column.wet()?(patch==0?Blocks.CLAY:Blocks.MUD):(patch==0?Blocks.MUD:Blocks.GRASS_BLOCK)).defaultBlockState();
|
||||
}
|
||||
/** Narrow exception for native marsh plants above protected water; never admits soil/tree writes. */
|
||||
public static boolean allowsWetlandPlant(NoiseBasedChunkGenerator generator,RandomState state,BlockPos pos,BlockState replacement){
|
||||
var c=wetlandPlan(generator,state,PopulationHydrologyRuntime.capacity(generator).regionAt(pos.getX(),pos.getZ()))
|
||||
.column(pos.getX(),pos.getY(),pos.getZ());
|
||||
if(c==null || pos.getY()<=Math.max(c.floorY(),c.waterY())
|
||||
|| pos.getY()>(replacement.is(Blocks.VINE)?c.maxY():Math.max(c.floorY(),c.waterY())+3))return false;
|
||||
return replacement.is(Blocks.LILY_PAD)||replacement.is(Blocks.SUGAR_CANE)||replacement.is(Blocks.SHORT_GRASS)
|
||||
|| replacement.is(Blocks.TALL_GRASS)||replacement.is(Blocks.BLUE_ORCHID)||replacement.is(Blocks.FIREFLY_BUSH)
|
||||
|| replacement.is(Blocks.BROWN_MUSHROOM)||replacement.is(Blocks.RED_MUSHROOM)||replacement.is(Blocks.VINE);
|
||||
}
|
||||
/** The water envelope and its bed are checked against FINAL density, including other cuts. */
|
||||
private static boolean retained(Terrain25Density.View density, Terrain25Shape.Chamber chamber) {
|
||||
int water = chamber.fluidY(), floor = chamber.floorY();
|
||||
int wet = 0;
|
||||
for (int x = (int) (chamber.x() - chamber.rx() - 2); x <= chamber.x() + chamber.rx() + 2; x++)
|
||||
for (int z = (int) (chamber.z() - chamber.rz() - 2); z <= chamber.z() + chamber.rz() + 2; z++) {
|
||||
if (chamber.cut(x, water, z) <= 0) continue;
|
||||
wet++;
|
||||
if (density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor, z) <= 0
|
||||
|| density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor - 1, z) <= 0) {
|
||||
SanctuaryMod.LOGGER.info("Terrain25 basin {} {} rejected: unsupported floor at {},{},{} (natural={})", chamber.id(),chamber.kind(),x,floor,z,
|
||||
density.natural(SamplerContext.EMPTY_UNCACHED,x,floor,z));
|
||||
return false;
|
||||
}
|
||||
for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}})
|
||||
for (int y = floor + 1; y <= water; y++)
|
||||
if (chamber.cut(x + d[0], y, z + d[1]) <= 0
|
||||
&& density.sampleValue(SamplerContext.EMPTY_UNCACHED, x + d[0], y, z + d[1]) <= 0) {
|
||||
SanctuaryMod.LOGGER.info("Terrain25 basin {} {} rejected: open bank at {},{},{} (natural={})",chamber.id(),chamber.kind(),x+d[0],y,z+d[1],
|
||||
density.natural(SamplerContext.EMPTY_UNCACHED,x+d[0],y,z+d[1]));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return wet >= 50;
|
||||
}
|
||||
private static boolean legacyFluidSafe(NoiseBasedChunkGenerator generator, RandomState state, Terrain25Shape.Chamber chamber) {
|
||||
int minX=(int)(chamber.x()-chamber.rx()-8), maxX=(int)(chamber.x()+chamber.rx()+8);
|
||||
int minZ=(int)(chamber.z()-chamber.rz()-8), maxZ=(int)(chamber.z()+chamber.rz()+8);
|
||||
for(var region:PopulationHydrologyRuntime.plansIntersecting(generator,state,minX,minZ,maxX,maxZ)) {
|
||||
// A same-fluid excavation can drain a basin too: certify against all older cuts,
|
||||
// not only heat/cold conflicts. Joining a different retained waterline is not implicit.
|
||||
for(var cell:region.water().cells()) if(cell.x()>=minX && cell.x()<=maxX && cell.z()>=minZ && cell.z()<=maxZ
|
||||
&& cell.bedY()<=chamber.fluidY()+3 && Math.max(cell.waterY(),cell.carveTop())>=chamber.floorY()-3) return false;
|
||||
for(var source:region.water().springs()) if(source.flowBounds().minX()<=maxX && source.flowBounds().maxX()>=minX
|
||||
&& source.flowBounds().minZ()<=maxZ && source.flowBounds().maxZ()>=minZ
|
||||
&& source.flowBounds().minY()<=chamber.fluidY()+3 && source.flowBounds().maxY()>=chamber.floorY()-3) return false;
|
||||
for(var source:region.water().spills()) if(source.flowBounds().minX()<=maxX && source.flowBounds().maxX()>=minX
|
||||
&& source.flowBounds().minZ()<=maxZ && source.flowBounds().maxZ()>=minZ
|
||||
&& source.flowBounds().minY()<=chamber.fluidY()+3 && source.flowBounds().maxY()>=chamber.floorY()-3) return false;
|
||||
for(var cell:region.lava().cells()) if(cell.x()>=minX && cell.x()<=maxX && cell.z()>=minZ && cell.z()<=maxZ
|
||||
&& cell.y()<=chamber.fluidY()+3 && cell.y()>=chamber.floorY()-3) return false;
|
||||
for(var bounds:region.lava().flowBounds()) if(bounds.minX()<=maxX && bounds.maxX()>=minX
|
||||
&& bounds.minZ()<=maxZ && bounds.maxZ()>=minZ && bounds.minY()<=chamber.fluidY()+8
|
||||
&& bounds.maxY()>=chamber.floorY()-8) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static List<Fall> falls(NoiseBasedChunkGenerator generator, RandomState state, IslandCapacity.Region region) {
|
||||
var world = world(generator, state);
|
||||
return world.falls.getOrCreate(region, () -> {
|
||||
List<Fall> result = new ArrayList<>();
|
||||
var plan = PopulationHydrologyRuntime.regionPlan(generator, state, region).water();
|
||||
for (var river : plan.features()) if (river.kind() == PopulationHydrology.Kind.RIVER && river.path().size() >= 2) {
|
||||
for (var endpoint : List.of(river.path().getFirst(), river.path().getLast())) {
|
||||
Fall best = null; double bestLength = 65;
|
||||
for (int direction = 0; direction < 32; direction++) {
|
||||
double angle = direction * Math.PI / 16;
|
||||
for (int length = 8; length <= 64; length += 4) {
|
||||
int ex = endpoint.x() + (int) Math.round(Math.cos(angle) * length);
|
||||
int ez = endpoint.z() + (int) Math.round(Math.sin(angle) * length);
|
||||
if (world.column(ex, ez).top >= river.waterY() - 10) continue;
|
||||
int width = 8 + Math.floorMod(mix(state.seed() ^ river.id() ^ key(ex, ez)), 9);
|
||||
Fall candidate = new Fall(river.id(), endpoint.x(), endpoint.z(), ex, ez, river.waterY(), width);
|
||||
if (length < bestLength && admissible(world, plan, candidate)) { best = candidate; bestLength = length; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (best != null) result.add(best);
|
||||
else SanctuaryMod.LOGGER.debug("Terrain25 river {} endpoint {},{}: no retained outlet within 64 blocks", river.id(), endpoint.x(), endpoint.z());
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
});
|
||||
}
|
||||
private static boolean admissible(World world, PopulationHydrology.Plan water, Fall fall) {
|
||||
int bed = fall.waterY - 3;
|
||||
for (int x = fall.minX(); x <= fall.maxX(); x++) for (int z = fall.minZ(); z <= fall.maxZ(); z++) {
|
||||
if (!fall.channel(x, z)) continue;
|
||||
double t = fall.progress(x, z);
|
||||
if (Math.hypot(x, z) < 64) return false;
|
||||
int top = world.column(x, z).top;
|
||||
if (top > fall.waterY + 12) return false;
|
||||
if (t < .82 && (world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, bed, z) <= 0
|
||||
|| world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, bed - 1, z) <= 0)) return false;
|
||||
// Existing adjacent catchments may be connected only at this river's own waterline.
|
||||
for (var cell : water.cellsAt(x, z)) if (cell.hasWater() && cell.waterY() != fall.waterY) return false;
|
||||
if (t < .9) for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
|
||||
int nx = x + d[0], nz = z + d[1];
|
||||
if (!fall.channel(nx, nz) && water.cellsAt(nx, nz).isEmpty()
|
||||
&& world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, nx, fall.waterY, nz) <= 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static List<Fall> nearbyFalls(NoiseBasedChunkGenerator generator, RandomState state, int x, int z) {
|
||||
var cap = PopulationHydrologyRuntime.capacity(generator);
|
||||
List<Fall> result = new ArrayList<>();
|
||||
var first = cap.regionAt(x - 80, z - 80); var last = cap.regionAt(x + 80, z + 80);
|
||||
for (int rx = first.x(); rx <= last.x(); rx++) for (int rz = first.z(); rz <= last.z(); rz++) {
|
||||
var region = new IslandCapacity.Region(rx, rz);
|
||||
double nx = Math.max(0, Math.max(region.minX(), -region.maxX())), nz = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nx, nz) < cap.terrainLimit()) result.addAll(falls(generator, state, region));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/** Null means no additional alpha.25 replacement; original contains inherited water/materials. */
|
||||
public static BlockState finalBlock(NoiseBasedChunkGenerator generator, RandomState state, int x, int y, int z, BlockState original) {
|
||||
var world = world(generator, state);
|
||||
var wetland=wetlandPlan(generator,state,PopulationHydrologyRuntime.capacity(generator).regionAt(x,z)).columns(x,z);
|
||||
return finalBlock(world, nearbyFalls(generator, state, x, z), world.materials(x,z),wetland, state.seed(), x, y, z, original);
|
||||
}
|
||||
private static BlockState finalBlock(World world, List<Fall> falls, MaterialColumn materials, List<Terrain25Wetlands.Column> wetlands,
|
||||
long seed, int x, int y, int z, BlockState original) {
|
||||
for (Fall fall : falls) if (fall.channel(x, z)) {
|
||||
if (y > fall.waterY - 3 && y <= fall.waterY + 13) {
|
||||
if (y > fall.waterY) return Blocks.AIR.defaultBlockState();
|
||||
// The lip alone releases water; no prefilled falling column or global sea layer.
|
||||
return world.density.sampleValue(SamplerContext.EMPTY_UNCACHED,x,fall.waterY-3,z)>0
|
||||
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
if (y == fall.waterY - 3 && fall.progress(x,z) < .82) return Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
for (var lake : world.lakes) if (y > lake.floorY() && y <= lake.fluidY() && lake.cut(x, y, z) > 0)
|
||||
return (lake.kind() == Terrain25Shape.Kind.LAVA ? Blocks.LAVA : Blocks.WATER).defaultBlockState();
|
||||
if (original.isAir() || !original.getFluidState().isEmpty()) return original;
|
||||
for(var wetland:wetlands) {
|
||||
var shore=shoreMaterial(wetland,seed,y,original);
|
||||
if(shore!=original)return shore;
|
||||
}
|
||||
var chamber = world.density.plan().chamberAt(x, y, z);
|
||||
if (chamber != null && chamber.kind() == Terrain25Shape.Kind.MUSHROOM && y == chamber.floorY()
|
||||
&& chamber.horizontal(x,z) < .62 && world.density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, y + 1, z) <= 0) {
|
||||
int patch = Math.floorMod(mix(seed ^ key(Math.floorDiv(x, 8), Math.floorDiv(z,8))), 5);
|
||||
return (patch < 2 ? Blocks.MYCELIUM : patch < 4 ? Blocks.MOSS_BLOCK : Blocks.PODZOL).defaultBlockState();
|
||||
}
|
||||
if (!(original.is(Blocks.STONE) || original.is(Blocks.DEEPSLATE) || original.is(Blocks.ANDESITE)
|
||||
|| original.is(Blocks.DIORITE) || original.is(Blocks.GRANITE))) return original;
|
||||
Column column = materials.geological;
|
||||
if (materials.shell != null && Terrain25Coating.calcite(x,y,z,column.lift,materials.shell))
|
||||
return Blocks.CALCITE.defaultBlockState();
|
||||
if (column.top - column.bottom < 24) return original;
|
||||
double depth = Terrain25Shape.depth(y, column.bottom, column.top);
|
||||
int gx = Math.floorDiv(x, 14), gy = Math.floorDiv(y, 12), gz = Math.floorDiv(z, 14);
|
||||
long h = mix(seed ^ key(gx, gz) ^ ((long) gy * 0x25BA5A17L));
|
||||
int cx = gx * 14 + 3 + Math.floorMod(h, 8), cy = gy * 12 + 3 + Math.floorMod(h >>> 9, 6), cz = gz * 14 + 3 + Math.floorMod(h >>> 18, 8);
|
||||
double dx = (x - cx) / 4.3, dy = (y - cy) / 2.8, dz = (z - cz) / 3.3;
|
||||
if (dx * dx + dy * dy + dz * dz >= 1) return original;
|
||||
int choice = Math.floorMod(h >>> 27, 100);
|
||||
boolean deep = original.is(Blocks.DEEPSLATE) || depth > .72;
|
||||
if (choice < 33) return depth < .75 ? (deep ? Blocks.DEEPSLATE_COAL_ORE : Blocks.COAL_ORE).defaultBlockState() : original;
|
||||
if (choice < 53) return (deep ? Blocks.DEEPSLATE_IRON_ORE : Blocks.IRON_ORE).defaultBlockState();
|
||||
if (choice < 72) return depth < .65 ? Blocks.COPPER_ORE.defaultBlockState() : original;
|
||||
if (choice < 81) return depth > .42 ? (deep ? Blocks.DEEPSLATE_REDSTONE_ORE : Blocks.REDSTONE_ORE).defaultBlockState() : original;
|
||||
if (choice < 86) return depth > .35 ? (deep ? Blocks.DEEPSLATE_GOLD_ORE : Blocks.GOLD_ORE).defaultBlockState() : original;
|
||||
if (choice < 91) return depth > .3 ? (deep ? Blocks.DEEPSLATE_LAPIS_ORE : Blocks.LAPIS_ORE).defaultBlockState() : original;
|
||||
if (choice < 94) return depth > .65 ? (deep ? Blocks.DEEPSLATE_DIAMOND_ORE : Blocks.DIAMOND_ORE).defaultBlockState() : original;
|
||||
if (choice == 95) return depth > .5 ? (deep ? Blocks.DEEPSLATE_EMERALD_ORE : Blocks.EMERALD_ORE).defaultBlockState() : original;
|
||||
return original;
|
||||
}
|
||||
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState state, BlockPos base, int radius, int height, int rootDepth) {
|
||||
if (PopulationHydrologyRuntime.protects(generator, state, base, radius, height, rootDepth)) return true;
|
||||
var world = world(generator,state);
|
||||
for (var lake : world.lakes) if (base.getY()-rootDepth <= lake.fluidY()+2 && base.getY()+height >= lake.floorY()-2
|
||||
&& Math.abs(base.getX()-lake.x()) <= lake.rx()+radius+2 && Math.abs(base.getZ()-lake.z()) <= lake.rz()+radius+2) return true;
|
||||
for (var fall : nearbyFalls(generator,state,base.getX(),base.getZ()))
|
||||
if (Terrain25FallProtection.intersects(fall, base.getX()-radius, base.getY()-rootDepth, base.getZ()-radius,
|
||||
base.getX()+radius, base.getY()+height, base.getZ()+radius)) return true;
|
||||
return false;
|
||||
}
|
||||
/** Called once per base column, not once per block, to reuse indexed regional plans. */
|
||||
public static net.minecraft.world.level.NoiseColumn applyColumn(NoiseBasedChunkGenerator generator, RandomState state,
|
||||
int x, int z, net.minecraft.world.level.NoiseColumn original) {
|
||||
var world = world(generator,state); var falls = nearbyFalls(generator,state,x,z);
|
||||
var cap = PopulationHydrologyRuntime.capacity(generator);
|
||||
if (Math.hypot(x,z) < cap.terrainLimit()) {
|
||||
var region = PopulationHydrologyRuntime.regionPlan(generator,state,cap.regionAt(x,z));
|
||||
for (var cell : region.water().cellsAt(x,z)) {
|
||||
for (int d=0;d<cell.sedimentDepth();d++) original.setBlock(cell.bedY()-d,PopulationMaterials.sedimentBlock(cell,state.seed(),d));
|
||||
for (int y=cell.bedY()+1;y<=Math.max(cell.waterY(),cell.carveTop());y++)
|
||||
original.setBlock(y,(cell.hasWater() && y<=cell.waterY()?Blocks.WATER:Blocks.AIR).defaultBlockState());
|
||||
}
|
||||
for (var spring : region.water().springs()) if (spring.source().x()==x && spring.source().z()==z)
|
||||
original.setBlock(spring.source().y(),Blocks.WATER.defaultBlockState());
|
||||
for (var cell : region.lava().cellsInChunk(x>>4,z>>4)) if(cell.x()==x && cell.z()==z)
|
||||
original.setBlock(cell.y(),(cell.lava()?Blocks.LAVA:Blocks.AIR).defaultBlockState());
|
||||
}
|
||||
var materials=world.materials(x,z);
|
||||
var wetland=wetlandPlan(generator,state,cap.regionAt(x,z)).columns(x,z);
|
||||
for (int y=0;y<384;y++) original.setBlock(y,finalBlock(world,falls,materials,wetland,state.seed(),x,y,z,original.getBlock(y)));
|
||||
return original;
|
||||
}
|
||||
public static void apply(NoiseBasedChunkGenerator generator, RandomState state, ChunkAccess chunk) {
|
||||
PopulationHydrologyRuntime.apply(generator,state,chunk);
|
||||
var world = world(generator,state); var cp = chunk.getPos();
|
||||
var falls = nearbyFalls(generator,state,cp.getMinBlockX()+8,cp.getMinBlockZ()+8);
|
||||
var wetlands=wetlandPlan(generator,state,PopulationHydrologyRuntime.capacity(generator).regionAt(cp.getMinBlockX(),cp.getMinBlockZ()));
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int x = cp.getMinBlockX(); x <= cp.getMaxBlockX(); x++) for (int z = cp.getMinBlockZ(); z <= cp.getMaxBlockZ(); z++) {
|
||||
var materials=world.materials(x,z);
|
||||
var wetland=wetlands.columns(x,z);
|
||||
for (int y=1; y<384; y++) {
|
||||
pos.set(x,y,z); var original = chunk.getBlockState(pos);
|
||||
var replacement = finalBlock(world,falls,materials,wetland,state.seed(),x,y,z,original);
|
||||
if (replacement != original) {
|
||||
chunk.setBlockState(pos,replacement,0);
|
||||
if (!replacement.getFluidState().isEmpty()) chunk.markPosForPostProcessing(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
Heightmap.primeHeightmaps(chunk,EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
}
|
||||
private static long key(int x,int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
|
||||
private static long mix(long n) { n=(n^(n>>>30))*0xbf58476d1ce4e5b9L; n=(n^(n>>>27))*0x94d049bb133111ebL; return n^(n>>>31); }
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Random;
|
||||
|
||||
/** Immutable, seed-local analytic terrain. No chunk or mutable generation order enters this plan. */
|
||||
public final class Terrain25Shape {
|
||||
public static final int MAX_TERRAIN_Y = 358;
|
||||
public enum Kind { DRY, LAKE, MUSHROOM, LAVA }
|
||||
public record Peak(double x, double z, double radius, double lift) {}
|
||||
public record Chamber(int id, double x, double y, double z, double rx, double ry, double rz, Kind kind) {
|
||||
public int floorY() { return (int) Math.floor(y - ry * .62); }
|
||||
public int fluidY() { return floorY() + (kind == Kind.LAKE ? 5 : kind == Kind.LAVA ? 3 : 0); }
|
||||
public double horizontal(double px, double pz) {
|
||||
double dx = (px - x) / rx, dz = (pz - z) / rz;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
public double cut(double px, double py, double pz) {
|
||||
double horizontal = horizontal(px, pz);
|
||||
double irregular = 1 + .07 * Math.sin(px * .17 + pz * .11) * Math.cos(pz * .19 - py * .13);
|
||||
if (py <= floorY()) return -1;
|
||||
double vertical = (py - y) / ry;
|
||||
return (irregular - horizontal - vertical * vertical) * .55;
|
||||
}
|
||||
}
|
||||
public record Passage(Chamber from, Chamber to, double width, double phase) {
|
||||
public double cut(double x, double y, double z) {
|
||||
double dx = to.x - from.x, dz = to.z - from.z;
|
||||
double t = Math.clamp(((x - from.x) * dx + (z - from.z) * dz) / (dx * dx + dz * dz), 0, 1);
|
||||
double side = Math.sin(t * Math.PI * 2 + phase) * 9 * Math.sin(t * Math.PI);
|
||||
double length = Math.sqrt(dx * dx + dz * dz);
|
||||
double cx = from.x + t * dx - dz / length * side, cz = from.z + t * dz + dx / length * side;
|
||||
double cy = from.y + (to.y - from.y) * t + Math.sin(t * Math.PI * 3 + phase) * 4 * Math.sin(t * Math.PI);
|
||||
double a = (x - cx) / width, b = (z - cz) / width, c = (y - cy) / (width * .8);
|
||||
return (1 - a * a - b * b - c * c) * .6;
|
||||
}
|
||||
}
|
||||
public record DryFit(int attempts, boolean certified, String diagnostic) {}
|
||||
public record Plan(long seed, double radius, List<Peak> peaks, List<Chamber> chambers, List<Passage> passages,
|
||||
Map<Integer,DryFit> dryFits) {
|
||||
public Plan {
|
||||
peaks = List.copyOf(peaks); chambers = List.copyOf(chambers); passages = List.copyOf(passages);
|
||||
dryFits = Map.copyOf(dryFits);
|
||||
}
|
||||
public Plan(long seed,double radius,List<Peak> peaks,List<Chamber> chambers,List<Passage> passages) {
|
||||
this(seed,radius,peaks,chambers,passages,Map.of());
|
||||
}
|
||||
public Set<Integer> certifiedDryFloors() {
|
||||
return dryFits.entrySet().stream().filter(e->e.getValue().certified()).map(Map.Entry::getKey)
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
/** Protects only an independently certified, existing three-block rock floor. */
|
||||
public Chamber dryFloor(int x,int y,int z) {
|
||||
for(Chamber chamber:chambers) {
|
||||
var fit=dryFits.get(chamber.id);
|
||||
if(fit==null||!fit.certified||y<chamber.floorY()-2||y>chamber.floorY())continue;
|
||||
if(chamber.cut(x,chamber.floorY()+1,z)>0)return chamber;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public double lift(int x, int z) {
|
||||
if (Math.hypot(x, z) < 64) return 0; // The arrival keeps its natural woodland scale.
|
||||
double result = 0;
|
||||
for (Peak peak : peaks) {
|
||||
double d = Math.hypot(x - peak.x, z - peak.z) / peak.radius;
|
||||
if (d < 1) {
|
||||
double shape = Math.pow(1 - d, 1.35);
|
||||
result = Math.max(result, peak.lift * shape);
|
||||
}
|
||||
}
|
||||
double arrival = Math.clamp((Math.hypot(x,z)-64)/32,0,1);
|
||||
return result * arrival * arrival * (3-2*arrival);
|
||||
}
|
||||
/** Monotonic vertical stretch: preserves cavities and prevents folded terrain. */
|
||||
public int naturalY(int x, int y, int z) { return naturalY(y, lift(x,z)); }
|
||||
public int naturalY(int y, double lift) {
|
||||
if (y <= 150) return y;
|
||||
return (int) Math.floor(150 + (y - 150) / (1 + lift / 95));
|
||||
}
|
||||
public Chamber retainingBasin(int x, int y, int z) {
|
||||
for (Chamber c : chambers) if ((c.kind == Kind.LAKE || c.kind == Kind.LAVA)
|
||||
&& c.horizontal(x,z) < 1.3 && y >= c.floorY()-3 && y <= c.fluidY()+2) return c;
|
||||
return null;
|
||||
}
|
||||
public double excavation(int x, int y, int z) {
|
||||
// Neighbouring dry rooms and slanting faults leave a natural membrane around a basin.
|
||||
// This changes excavation only; no missing natural rock is manufactured.
|
||||
var retained = retainingBasin(x,y,z);
|
||||
if (retained != null) return retained.cut(x,y,z);
|
||||
if (dryFloor(x,y,z) != null) return -1;
|
||||
double cut = -1;
|
||||
for (Chamber chamber : chambers) if (Math.abs(x - chamber.x) <= chamber.rx * 1.1
|
||||
&& Math.abs(z - chamber.z) <= chamber.rz * 1.1 && Math.abs(y - chamber.y) <= chamber.ry * 1.1)
|
||||
cut = Math.max(cut, chamber.cut(x, y, z));
|
||||
for (Passage passage : passages) if (x >= Math.min(passage.from.x, passage.to.x) - 18
|
||||
&& x <= Math.max(passage.from.x, passage.to.x) + 18
|
||||
&& z >= Math.min(passage.from.z, passage.to.z) - 18 && z <= Math.max(passage.from.z, passage.to.z) + 18
|
||||
&& y >= Math.min(passage.from.y, passage.to.y) - 12 && y <= Math.max(passage.from.y, passage.to.y) + 12)
|
||||
cut = Math.max(cut, passage.cut(x, y, z));
|
||||
return cut;
|
||||
}
|
||||
public Chamber chamberAt(int x, int y, int z) {
|
||||
for (Chamber chamber : chambers) if (chamber.horizontal(x, z) < .92
|
||||
&& y >= chamber.floorY() - 2 && y <= chamber.y + chamber.ry * .82) return chamber;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private Terrain25Shape() {}
|
||||
public static Plan create(long seed, double radius) {
|
||||
Random random = new Random(seed ^ 0xCA1C1725L);
|
||||
List<Peak> peaks = new ArrayList<>();
|
||||
double rotation = random.nextDouble() * Math.PI * 2;
|
||||
for (int group = 0; group < 3; group++) {
|
||||
double angle = rotation + group * Math.PI * 2 / 3 + (random.nextDouble() - .5) * .4;
|
||||
double distance = radius * (.38 + random.nextDouble() * .15);
|
||||
double gx = Math.cos(angle) * distance, gz = Math.sin(angle) * distance;
|
||||
for (int p = 0; p < 5; p++) {
|
||||
double offset = p == 0 ? 0 : 22 + random.nextDouble() * radius * .12;
|
||||
double bearing = random.nextDouble() * Math.PI * 2;
|
||||
peaks.add(new Peak(gx + Math.cos(bearing) * offset, gz + Math.sin(bearing) * offset,
|
||||
radius * (.13 + random.nextDouble() * .08), 115 + random.nextDouble() * 90));
|
||||
}
|
||||
}
|
||||
List<Chamber> chambers = new ArrayList<>();
|
||||
List<Passage> passages = new ArrayList<>();
|
||||
int groups = radius < 400 ? 4 : 5;
|
||||
for (int group = 0; group < groups; group++) {
|
||||
double angle = rotation + group * Math.PI * 2 / groups + .5;
|
||||
double distance = radius * (.23 + random.nextDouble() * .25);
|
||||
double cx = Math.cos(angle) * distance, cz = Math.sin(angle) * distance;
|
||||
Kind kind = Kind.values()[group % 4];
|
||||
double y = kind == Kind.LAVA ? 69 + random.nextInt(18) : 115 + random.nextInt(42);
|
||||
Chamber first = new Chamber(chambers.size(), cx, y, cz, 24 + random.nextInt(14),
|
||||
14 + random.nextInt(9), 22 + random.nextInt(15), kind);
|
||||
chambers.add(first);
|
||||
Chamber second = new Chamber(chambers.size(), cx + Math.cos(angle + 1) * 58,
|
||||
y - 4 + random.nextInt(9), cz + Math.sin(angle + 1) * 58,
|
||||
18 + random.nextInt(9), 12 + random.nextInt(7), 18 + random.nextInt(9), kind);
|
||||
chambers.add(second);
|
||||
passages.add(new Passage(first, second, kind == Kind.LAVA ? 3.5 : 4.5, random.nextDouble() * 6));
|
||||
}
|
||||
// A broad dry chamber exists independently of any village. Actual native structures
|
||||
// still certify its natural floor and roof; this is never a structure-shaped box.
|
||||
chambers.add(new Chamber(chambers.size(), Math.cos(rotation) * radius * .14, 151,
|
||||
Math.sin(rotation) * radius * .14, Math.min(104, radius * .27), 31,
|
||||
Math.min(94, radius * .25), Kind.DRY));
|
||||
return new Plan(seed, radius, peaks, chambers, passages);
|
||||
}
|
||||
@FunctionalInterface public interface Sampler { float sample(int x,int y,int z); }
|
||||
/** At most48 terrain proposals per basin/refuge. The shared intact field is sampled before any excavation. */
|
||||
public static Plan fit(Plan proposed, Sampler sampler) {
|
||||
var memo = new SignMemo(sampler);
|
||||
List<Chamber> fitted = new ArrayList<>();
|
||||
for (Chamber original : proposed.chambers) {
|
||||
if (original.kind == Kind.DRY) { fitted.add(original); continue; }
|
||||
Chamber chosen = null;
|
||||
for (int attempt=0;attempt<48;attempt++) {
|
||||
double angle = attempt * 2.399963229728653 + original.id * .7;
|
||||
double distance = attempt==0?0:Math.sqrt(attempt/47.0)*76;
|
||||
int x=(int)Math.round(original.x+Math.cos(angle)*distance);
|
||||
int z=(int)Math.round(original.z+Math.sin(angle)*distance);
|
||||
if (Math.hypot(x,z)>proposed.radius*.75)continue;
|
||||
int bestBottom=-1,bestTop=-1,runBottom=-1;
|
||||
for(int y=24;y<=286;y++) {
|
||||
if(memo.sample(x,y,z)>0) {if(runBottom<0)runBottom=y;}
|
||||
else if(runBottom>=0) {
|
||||
if(y-runBottom>original.ry*1.65+9 && (bestBottom<0 || (original.kind==Kind.LAVA?runBottom<bestBottom:y-runBottom>bestTop-bestBottom))) {
|
||||
bestBottom=runBottom;bestTop=y-1;
|
||||
}
|
||||
runBottom=-1;
|
||||
}
|
||||
}
|
||||
if(bestBottom<0)continue;
|
||||
int floor=(int)Math.round(original.kind==Kind.LAVA?bestBottom+8+(attempt%3)*6:bestBottom+(bestTop-bestBottom)*.28);
|
||||
double cy=floor+original.ry*.62+.1;
|
||||
Chamber candidate=new Chamber(original.id,x,cy,z,original.rx,original.ry,original.rz,original.kind);
|
||||
if(candidate.y+candidate.ry>bestTop-3)continue;
|
||||
if(fitted.stream().anyMatch(other -> fluidConflict(candidate,other)))continue;
|
||||
if(!coarseFloor(memo,candidate))continue;
|
||||
if((original.kind==Kind.LAKE || original.kind==Kind.LAVA) && !retainsNaturalFluid(memo,candidate))continue;
|
||||
chosen=candidate;break;
|
||||
}
|
||||
fitted.add(chosen==null?original:chosen);
|
||||
}
|
||||
// Dry rooms are fitted after the ecological basins, independently of all structures.
|
||||
// Their shape/radii are fixed already; only the location inside actual rock is searched.
|
||||
Map<Integer,DryFit> dryFits=new java.util.LinkedHashMap<>();
|
||||
for(Chamber original:proposed.chambers) if(original.kind==Kind.DRY) {
|
||||
Chamber chosen=null;int attempts=0;String diagnostic="no-solid-interval";
|
||||
for(int attempt=0;attempt<48;attempt++) {
|
||||
attempts++;
|
||||
int site=attempt/3,depth=attempt%3;
|
||||
double angle=site*2.399963229728653+original.id*.7;
|
||||
double distance=site==0?0:Math.sqrt(site/15.0)*Math.min(76,proposed.radius*.22);
|
||||
int x=(int)Math.round(original.x+Math.cos(angle)*distance);
|
||||
int z=(int)Math.round(original.z+Math.sin(angle)*distance);
|
||||
if(Math.hypot(x,z)>proposed.radius*.7){diagnostic="outside-central-rock";continue;}
|
||||
int bottom=-1,top=-1,run=-1;
|
||||
for(int y=24;y<=287;y++) {
|
||||
if(y<287&&memo.sample(x,y,z)>0){if(run<0)run=y;}
|
||||
else if(run>=0) {
|
||||
if(y-run>original.ry*1.65+9&&(bottom<0||y-run>top-bottom)){bottom=run;top=y-1;}
|
||||
run=-1;
|
||||
}
|
||||
}
|
||||
if(bottom<0){diagnostic="no-solid-interval";continue;}
|
||||
int floor=(int)Math.round(bottom+(top-bottom)*(.22+depth*.14));
|
||||
// Keep the original location when its whole natural floor was already suitable.
|
||||
if(attempt==0)floor=original.floorY();
|
||||
Chamber candidate=attempt==0?original:new Chamber(original.id,x,floor+original.ry*.62+.1,z,original.rx,original.ry,original.rz,Kind.DRY);
|
||||
if(floor<bottom+3||candidate.y+candidate.ry>top-3){diagnostic="insufficient-height";continue;}
|
||||
if(!coarseFloor(memo,candidate)){diagnostic="coarse-floor-or-roof";continue;}
|
||||
if(dryIntersectsFluid(candidate,fitted)){diagnostic="ecological-fluid-envelope";continue;}
|
||||
if(!continuousDryFloor(memo,candidate)){diagnostic="discontinuous-natural-floor";continue;}
|
||||
chosen=candidate;diagnostic="natural-three-layer-floor";break;
|
||||
}
|
||||
fitted.set(original.id,chosen==null?original:chosen);
|
||||
dryFits.put(original.id,new DryFit(attempts,chosen!=null,diagnostic));
|
||||
}
|
||||
List<Passage> passages = proposed.passages.stream().map(p -> new Passage(fitted.get(p.from.id),fitted.get(p.to.id),p.width,p.phase)).toList();
|
||||
return new Plan(proposed.seed,proposed.radius,proposed.peaks,fitted,passages,dryFits);
|
||||
}
|
||||
private static boolean continuousDryFloor(Sampler sample,Chamber chamber) {
|
||||
int floor=chamber.floorY(),columns=0;
|
||||
for(int x=(int)Math.floor(chamber.x-chamber.rx-2);x<=chamber.x+chamber.rx+2;x++)
|
||||
for(int z=(int)Math.floor(chamber.z-chamber.rz-2);z<=chamber.z+chamber.rz+2;z++) {
|
||||
if(chamber.cut(x,floor+1,z)<=0)continue;
|
||||
columns++;
|
||||
for(int y=floor-2;y<=floor;y++)if(sample.sample(x,y,z)<=0)return false;
|
||||
}
|
||||
return columns>=50;
|
||||
}
|
||||
private static boolean dryIntersectsFluid(Chamber dry,List<Chamber> fitted) {
|
||||
for(Chamber basin:fitted) {
|
||||
if(basin.kind!=Kind.LAKE&&basin.kind!=Kind.LAVA)continue;
|
||||
if(dry.floorY()-2>basin.fluidY()+3||dry.y+dry.ry<basin.floorY()-3
|
||||
||Math.abs(dry.x-basin.x)>dry.rx+basin.rx+3||Math.abs(dry.z-basin.z)>dry.rz+basin.rz+3)continue;
|
||||
// A coarse overlap first; dense footprint confirmation includes the reservoir's
|
||||
// natural membrane. Fluid rooms retain priority even if they are later refused.
|
||||
for(int x=(int)(basin.x-basin.rx-3);x<=basin.x+basin.rx+3;x++)
|
||||
for(int z=(int)(basin.z-basin.rz-3);z<=basin.z+basin.rz+3;z++) {
|
||||
if(basin.horizontal(x,z)>1.3)continue;
|
||||
for(int y=Math.max(dry.floorY()-2,basin.floorY()-3);y<=basin.fluidY()+3;y++)
|
||||
if(dry.cut(x,Math.max(dry.floorY()+1,y),z)>0)return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/** Unlike-kind reservoirs retain at least six rock blocks between their filled envelopes. */
|
||||
public static boolean fluidConflict(Chamber a,Chamber b) {
|
||||
boolean opposite=(a.kind==Kind.LAKE && b.kind==Kind.LAVA)||(a.kind==Kind.LAVA && b.kind==Kind.LAKE);
|
||||
return opposite && Math.abs(a.x-b.x)<a.rx+b.rx+6 && Math.abs(a.z-b.z)<a.rz+b.rz+6
|
||||
&& a.floorY()<b.fluidY()+6 && b.floorY()<a.fluidY()+6;
|
||||
}
|
||||
private static boolean coarseFloor(Sampler sample,Chamber c) {
|
||||
int floor=c.floorY(),roof=(int)(c.y+c.ry+2),ceilings=0;
|
||||
for(int ix=-3;ix<=3;ix++)for(int iz=-3;iz<=3;iz++) {
|
||||
double dx=ix/3.0,dz=iz/3.0;if(dx*dx+dz*dz>.85)continue;
|
||||
int x=(int)Math.round(c.x+dx*c.rx),z=(int)Math.round(c.z+dz*c.rz);
|
||||
if(sample.sample(x,floor,z)<=0 || sample.sample(x,floor-1,z)<=0)return false;
|
||||
if(sample.sample(x,roof,z)>0)ceilings++;
|
||||
}
|
||||
return ceilings >= (c.kind == Kind.MUSHROOM ? 17 : 14);
|
||||
}
|
||||
private static boolean retainsNaturalFluid(Sampler sample,Chamber c) {
|
||||
int floor=c.floorY(),water=c.fluidY();
|
||||
for(int x=(int)(c.x-c.rx-2);x<=c.x+c.rx+2;x++)for(int z=(int)(c.z-c.rz-2);z<=c.z+c.rz+2;z++) {
|
||||
if(c.cut(x,water,z)<=0)continue;
|
||||
if(sample.sample(x,floor,z)<=0 || sample.sample(x,floor-1,z)<=0)return false;
|
||||
for(int[] d:DIRECTIONS)for(int y=floor+1;y<=water;y++)
|
||||
if(c.cut(x+d[0],y,z+d[1])<=0 && sample.sample(x+d[0],y,z+d[1])<=0)return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static final int[][] DIRECTIONS={{1,0},{-1,0},{0,1},{0,-1}};
|
||||
private static final class SignMemo implements Sampler {
|
||||
private final Sampler source;
|
||||
private final java.util.Map<Long,long[]> columns = new java.util.LinkedHashMap<>(1024,.75f,true) {
|
||||
@Override protected boolean removeEldestEntry(java.util.Map.Entry<Long,long[]> eldest){return size()>32768;}
|
||||
};
|
||||
SignMemo(Sampler source){this.source=source;}
|
||||
@Override public float sample(int x,int y,int z) {
|
||||
if(y<0||y>=384)return -1;
|
||||
long key=((long)x<<32)^(z&0xffffffffL);long[] bits=columns.computeIfAbsent(key,ignored->new long[12]);
|
||||
int word=y>>>6;long mask=1L<<(y&63);
|
||||
if((bits[word]&mask)==0){bits[word]|=mask;if(source.sample(x,y,z)>0)bits[word+6]|=mask;}
|
||||
return (bits[word+6]&mask)!=0?1:-1;
|
||||
}
|
||||
}
|
||||
/** Geological depth is measured in the intact column, never against an excavated cave floor. */
|
||||
public static double depth(int y, int naturalBottom, int naturalTop) {
|
||||
return Math.clamp((naturalTop - y) / (double) Math.max(1, naturalTop - naturalBottom), 0, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Conservative native-tree envelope: foliage can extend beyond the last trunk attachment. */
|
||||
public final class Terrain25TreeLimits {
|
||||
public static final int MAX_TREE_Y = 363;
|
||||
private Terrain25TreeLimits() {}
|
||||
public static boolean fits(int trunkY, int trunkHeight, int maximumFoliageOffset) {
|
||||
return (long) trunkY + trunkHeight + Math.max(0, maximumFoliageOffset) + 2 <= MAX_TREE_Y;
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.lighting.LightEngine;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
|
||||
/** Conservative mushroom admission before FEATURES has received its final native lighting. */
|
||||
public final class Terrain25WetlandPlantLight {
|
||||
private Terrain25WetlandPlantLight() {}
|
||||
|
||||
public static boolean lightSafe(BlockGetter level, BlockPos pos, BlockState plant) {
|
||||
if (!mushroom(plant)) return true;
|
||||
return lightSafe(level, pos, plant,
|
||||
level.getBlockState(pos.below()).is(BlockTags.OVERRIDES_MUSHROOM_LIGHT_REQUIREMENT));
|
||||
}
|
||||
|
||||
static boolean lightSafe(BlockGetter level, BlockPos pos, BlockState plant, boolean substrateOverridesLight) {
|
||||
if (!mushroom(plant) || substrateOverridesLight) return true;
|
||||
// Propagated sky/block light loses at least one level per step. Consequently
|
||||
// only direct sky15 or emitters within Manhattan distance2 can reach level13.
|
||||
// We deliberately ignore obstacles along these short paths: a safe upper bound,
|
||||
// not a replacement lighting engine and not a claim of exact brightness.
|
||||
var cursor = new BlockPos.MutableBlockPos();
|
||||
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
|
||||
int horizontal = Math.abs(dx) + Math.abs(dz);
|
||||
if (horizontal > 2) continue;
|
||||
int reach = 2 - horizontal;
|
||||
int x = pos.getX() + dx, z = pos.getZ() + dz;
|
||||
// If any reachable height in this column is a direct sky source, its
|
||||
// highest reachable height is too. Thus each of the13 columns is read once.
|
||||
if (directSky(level, x, pos.getY() + reach, z, cursor)) return false;
|
||||
for (int dy = -reach; dy <= reach; dy++) {
|
||||
int y = pos.getY() + dy;
|
||||
if (level.isOutsideBuildHeight(y)) continue;
|
||||
int emission = level.getBlockState(cursor.set(x, y, z)).getLightEmission();
|
||||
if (emission - horizontal - Math.abs(dy) >= 13) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean directSky(BlockGetter level, int x, int y, int z, BlockPos.MutableBlockPos cursor) {
|
||||
if (y > level.getMaxY()) return true;
|
||||
if (y < level.getMinY()) return false;
|
||||
var below = level.getBlockState(cursor.set(x, y, z));
|
||||
// Scan upward so a nearby cave roof rejects the source immediately. At most
|
||||
// world height +1 reads per column; no cache can go stale as decoration proceeds.
|
||||
for (int aboveY = y + 1; aboveY <= level.getMaxY() + 1; aboveY++) {
|
||||
if (below.getLightDampening() != 0) return false;
|
||||
var above = aboveY > level.getMaxY() ? Blocks.AIR.defaultBlockState()
|
||||
: level.getBlockState(cursor.set(x, aboveY, z));
|
||||
// The same edge rule as native ChunkSkyLightSources in26.3-pre-2.
|
||||
if (Shapes.faceShapeOccludes(LightEngine.getOcclusionShape(above, Direction.DOWN),
|
||||
LightEngine.getOcclusionShape(below, Direction.UP))) return false;
|
||||
below = above;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean mushroom(BlockState plant) {
|
||||
return plant.is(Blocks.BROWN_MUSHROOM) || plant.is(Blocks.RED_MUSHROOM);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.VineBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Small local botany pass: native heightmap placements alone cannot decorate a lake under a roof. */
|
||||
public final class Terrain25WetlandVegetation {
|
||||
private Terrain25WetlandVegetation() {}
|
||||
public static boolean place(WorldGenLevel level,NoiseBasedChunkGenerator generator,RandomState state,int minX,int minZ) {
|
||||
var plan=Terrain25Runtime.wetlandPlan(generator,state,PopulationHydrologyRuntime.capacity(generator).regionAt(minX,minZ));
|
||||
boolean changed=false;
|
||||
// All writes stay in the current chunk; the one-block rim keeps native survival probes local too.
|
||||
for(int x=minX+1;x<minX+15;x++)for(int z=minZ+1;z<minZ+15;z++)for(var column:plan.columns(x,z)) {
|
||||
if(!column.underground())continue;
|
||||
int base=Math.max(column.floorY(),column.waterY());
|
||||
BlockPos at=new BlockPos(x,base+1,z);
|
||||
if(!level.getBiome(at).is(Terrain25Biomes.SWAMP))continue;
|
||||
long h=WoodlandGroveGeometry.seed(state.seed(),x,column.waterY(),z,0x25CA7E5L);
|
||||
if(column.wet()) {
|
||||
if(Math.floorMod(h,31)==0)changed|=plant(level,at,Blocks.LILY_PAD.defaultBlockState());
|
||||
} else {
|
||||
int pick=Math.floorMod(h,32);
|
||||
BlockState plant=pick<2?Blocks.FIREFLY_BUSH.defaultBlockState():pick<4?Blocks.BLUE_ORCHID.defaultBlockState():
|
||||
pick<10?Blocks.SHORT_GRASS.defaultBlockState():pick==10?
|
||||
(Math.floorMod(h>>>10,2)==0?Blocks.BROWN_MUSHROOM:Blocks.RED_MUSHROOM).defaultBlockState():null;
|
||||
if(plant!=null)changed|=plant(level,at,plant);
|
||||
if(Math.floorMod(h>>>16,5)==0)for(int y=at.getY();y<=Math.min(column.maxY(),at.getY()+Math.floorMod(h>>>22,3));y++) {
|
||||
if(!plant(level,new BlockPos(x,y,z),Blocks.SUGAR_CANE.defaultBlockState()))break;
|
||||
changed=true;
|
||||
}
|
||||
}
|
||||
if(Math.floorMod(h>>>29,23)==0) {
|
||||
// Actual stone/soil must support the first vine; no floating decorative attachment.
|
||||
BlockPos vineAt=new BlockPos(x,column.maxY(),z);
|
||||
BlockState vine=Blocks.VINE.defaultBlockState();boolean attached=false,wall=false;
|
||||
for(var direction:Direction.Plane.HORIZONTAL)if(VineBlock.isAcceptableNeighbour(level,vineAt.relative(direction),direction)) {
|
||||
vine=vine.setValue(VineBlock.getPropertyForFace(direction),true);attached=true;wall=true;
|
||||
}
|
||||
if(VineBlock.isAcceptableNeighbour(level,vineAt.above(),Direction.UP)){vine=vine.setValue(VineBlock.UP,true);attached=true;}
|
||||
if(attached&&plant(level,vineAt,vine)) {
|
||||
changed=true;
|
||||
if(wall)for(int drop=1;drop<=2&&vineAt.getY()-drop>base;drop++) {
|
||||
if(!plant(level,vineAt.below(drop),vine.setValue(VineBlock.UP,false)))break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
private static boolean plant(WorldGenLevel level,BlockPos pos,BlockState plant) {
|
||||
return level.getBlockState(pos).isAir()&&plant.canSurvive(level,pos)&&level.setBlock(pos,plant,2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Local wetlands follow retained water and existing shores, including independent underground strata. */
|
||||
public final class Terrain25Wetlands {
|
||||
public static final int MAX_CANDIDATES = 64;
|
||||
public static final int MAX_SITES = 3;
|
||||
public static final int BANK_REACH = 18;
|
||||
public static final int SITE_RADIUS = 28;
|
||||
public static final int ARRIVAL_RADIUS = 96;
|
||||
public static final int MAX_UNDERGROUND_SITES = 3;
|
||||
@FunctionalInterface public interface Lookup { boolean contains(int x, int y, int z); }
|
||||
public interface Terrain {
|
||||
int top(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
double lift(int x, int z);
|
||||
boolean excluded(int x, int minY, int maxY, int z);
|
||||
}
|
||||
public record Column(int x, int z, int floorY, int waterY, boolean wet, int minY, int maxY,
|
||||
long waterFeature, boolean underground) {
|
||||
public boolean contains(int y) { return y >= minY && y <= maxY; }
|
||||
}
|
||||
public record Site(long id, long waterFeature, int x, int z, int waterY,
|
||||
int minX, int minZ, int maxX, int maxZ, int wetColumns, int bankColumns, boolean underground) {}
|
||||
public static final class Plan implements Lookup {
|
||||
public static final Plan EMPTY = new Plan(List.of(), List.of(), 0, Map.of());
|
||||
private final List<Site> sites;
|
||||
private final Map<Long,List<Column>> columns;
|
||||
private final List<Column> allColumns;
|
||||
private final int candidates;
|
||||
private final Map<String,Integer> refusals;
|
||||
private Plan(List<Site> sites, Collection<Column> values, int candidates, Map<String,Integer> refusals) {
|
||||
this.sites = List.copyOf(sites);
|
||||
var indexed = new HashMap<Long,List<Column>>();
|
||||
this.allColumns = values.stream().distinct().sorted(Comparator.comparingInt(Column::x)
|
||||
.thenComparingInt(Column::z).thenComparingInt(Column::waterY).thenComparingLong(Column::waterFeature)).toList();
|
||||
for(var column:allColumns)indexed.computeIfAbsent(key(column.x(),column.z()),ignored->new ArrayList<>()).add(column);
|
||||
indexed.replaceAll((ignored,list)->List.copyOf(list));this.columns=Map.copyOf(indexed);
|
||||
this.candidates = candidates; this.refusals = Map.copyOf(refusals);
|
||||
}
|
||||
public List<Site> sites() { return sites; }
|
||||
public Collection<Column> columns() { return allColumns; }
|
||||
public List<Column> columns(int x,int z) { return columns.getOrDefault(key(x,z),List.of()); }
|
||||
/** Compatibility accessor for surface consumers; stratified consumers must select a Y/site. */
|
||||
public Column column(int x,int z) { var list=columns(x,z);return list.isEmpty()?null:list.getLast(); }
|
||||
public Column column(int x,int y,int z) { for(var c:columns(x,z))if(c.contains(y))return c;return null; }
|
||||
public Column columnAtWater(int x,int z,int waterY) { for(var c:columns(x,z))if(c.waterY()==waterY)return c;return null; }
|
||||
public Column column(Site site,int x,int z) {
|
||||
for(var c:columns(x,z))if(c.waterFeature()==site.waterFeature()&&c.waterY()==site.waterY())return c;return null;
|
||||
}
|
||||
public int candidates() { return candidates; }
|
||||
public Map<String,Integer> refusals() { return refusals; }
|
||||
@Override public boolean contains(int x, int y, int z) {
|
||||
return column(x,y,z)!=null;
|
||||
}
|
||||
}
|
||||
private record Step(int x, int z, int distance) {}
|
||||
private static final int[][] CARDINALS = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||
private Terrain25Wetlands() {}
|
||||
|
||||
/** Reads only a finite regional water plan and the shared final density; no biome/chunk access. */
|
||||
public static Plan create(long seed, Collection<PopulationHydrology.Cell> cells,
|
||||
List<PopulationHydrology.Feature> features, Terrain terrain) {
|
||||
Set<Long> calm = new HashSet<>();
|
||||
for (var f : features) if (f.kind() == PopulationHydrology.Kind.POND || f.kind() == PopulationHydrology.Kind.LAKE)
|
||||
calm.add(f.id());
|
||||
Map<Long,PopulationHydrology.Cell> surface = new HashMap<>();
|
||||
for (var cell : cells) surface.merge(key(cell.x(),cell.z()), cell,
|
||||
(a,b) -> Math.max(a.bedY(),a.waterY()) >= Math.max(b.bedY(),b.waterY()) ? a : b);
|
||||
List<PopulationHydrology.Cell> candidates = surface.values().stream()
|
||||
.filter(c -> c.hasWater() && calm.contains(c.featureId()) && c.waterY()-c.bedY() <= 4)
|
||||
.filter(c -> Math.hypot(c.x(),c.z()) >= ARRIVAL_RADIUS)
|
||||
.filter(c -> Arrays.stream(CARDINALS).anyMatch(d -> {
|
||||
var neighbour = surface.get(key(c.x()+d[0],c.z()+d[1]));
|
||||
return neighbour == null || !neighbour.hasWater() || neighbour.featureId() != c.featureId();
|
||||
}))
|
||||
.sorted(Comparator.comparingLong(c -> mix(seed ^ c.featureId() ^ key(c.x(),c.z()))))
|
||||
.limit(MAX_CANDIDATES).toList();
|
||||
var sites = new ArrayList<Site>(); var result = new LinkedHashMap<Long,Column>();
|
||||
var refusals = new LinkedHashMap<String,Integer>();
|
||||
Map<Long,Integer> topCache = new LinkedHashMap<>(1024,.75f,true) {
|
||||
@Override protected boolean removeEldestEntry(Map.Entry<Long,Integer> eldest){return size()>32768;}
|
||||
};
|
||||
int attempts = 0;
|
||||
for (var candidate : candidates) {
|
||||
if (sites.size() >= MAX_SITES) break;
|
||||
attempts++;
|
||||
if (sites.stream().anyMatch(s -> Math.hypot(candidate.x()-s.x(),candidate.z()-s.z()) < SITE_RADIUS*1.6)) {
|
||||
refused(refusals,"near-existing-wetland"); continue;
|
||||
}
|
||||
var centre = column(candidate.x(),candidate.z(),candidate.waterY(),candidate.featureId(),surface,terrain,topCache);
|
||||
if (centre == null || !centre.wet()) { refused(refusals,"not-exposed-shallow-water"); continue; }
|
||||
boolean shore = false;
|
||||
for (int[] d : CARDINALS) for (int distance = 2; distance <= 10 && !shore; distance += 2) {
|
||||
var bank = column(candidate.x()+d[0]*distance,candidate.z()+d[1]*distance,candidate.waterY(),candidate.featureId(),surface,terrain,topCache);
|
||||
if (bank != null && !bank.wet()) shore = true;
|
||||
}
|
||||
if (!shore) { refused(refusals,"no-gentle-natural-shore"); continue; }
|
||||
var patch = patch(candidate,surface,terrain,topCache);
|
||||
int wet = (int)patch.values().stream().filter(Column::wet).count(), banks = patch.size()-wet;
|
||||
if (wet < 12 || banks < 48) { refused(refusals,"insufficient-connected-water-and-bank"); continue; }
|
||||
// Sites stay local even along a long lake. Columns carry their own exact Y envelope.
|
||||
int minX=Integer.MAX_VALUE,minZ=Integer.MAX_VALUE,maxX=Integer.MIN_VALUE,maxZ=Integer.MIN_VALUE;
|
||||
for (var c : patch.values()) {
|
||||
minX=Math.min(minX,c.x()); minZ=Math.min(minZ,c.z());
|
||||
maxX=Math.max(maxX,c.x()); maxZ=Math.max(maxZ,c.z());
|
||||
}
|
||||
long id=mix(seed ^ candidate.featureId() ^ key(candidate.x(),candidate.z()));
|
||||
sites.add(new Site(id,candidate.featureId(),candidate.x(),candidate.z(),candidate.waterY(),minX,minZ,maxX,maxZ,wet,banks,false));
|
||||
patch.forEach(result::putIfAbsent);
|
||||
}
|
||||
return new Plan(sites,result.values(),attempts,refusals);
|
||||
}
|
||||
private static Map<Long,Column> patch(PopulationHydrology.Cell candidate, Map<Long,PopulationHydrology.Cell> cells,
|
||||
Terrain terrain, Map<Long,Integer> tops) {
|
||||
int cx=candidate.x(),cz=candidate.z(),water=candidate.waterY();
|
||||
var queue = new ArrayDeque<Step>(); var visited = new HashMap<Long,Integer>();
|
||||
var columns = new LinkedHashMap<Long,Column>();
|
||||
// Every source is actual retained shallow water at this waterline, within the local patch.
|
||||
for (var c : cells.values()) if (c.hasWater() && c.featureId()==candidate.featureId() && c.waterY()==water
|
||||
&& Math.hypot(c.x()-cx,c.z()-cz) <= SITE_RADIUS) {
|
||||
var column=column(c.x(),c.z(),water,candidate.featureId(),cells,terrain,tops);
|
||||
if(column!=null && column.wet()) { queue.addLast(new Step(c.x(),c.z(),0)); visited.put(key(c.x(),c.z()),0); }
|
||||
}
|
||||
while(!queue.isEmpty()) {
|
||||
var point=queue.removeFirst(); var current=column(point.x(),point.z(),water,candidate.featureId(),cells,terrain,tops);
|
||||
if(current==null)continue;
|
||||
columns.put(key(point.x(),point.z()),current);
|
||||
if(point.distance() >= BANK_REACH)continue;
|
||||
for(int[] d:CARDINALS) {
|
||||
int x=point.x()+d[0],z=point.z()+d[1],distance=point.distance()+1; long key=key(x,z);
|
||||
if(Math.hypot(x-cx,z-cz)>SITE_RADIUS || visited.getOrDefault(key,Integer.MAX_VALUE)<=distance)continue;
|
||||
var next=column(x,z,water,candidate.featureId(),cells,terrain,tops);
|
||||
if(next==null || Math.abs(Math.max(current.floorY(),water)-Math.max(next.floorY(),water))>1)continue;
|
||||
visited.put(key,distance);queue.addLast(new Step(x,z,distance));
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
private static Column column(int x,int z,int water,long feature,Map<Long,PopulationHydrology.Cell> cells,
|
||||
Terrain terrain,Map<Long,Integer> tops) {
|
||||
if(Math.hypot(x,z)<ARRIVAL_RADIUS || terrain.lift(x,z)>12)return null;
|
||||
int top=tops.computeIfAbsent(key(x,z),ignored -> terrain.top(x,z));
|
||||
if(top<0)return null;
|
||||
var cell=cells.get(key(x,z));
|
||||
boolean exposed=cell!=null && top<=cell.carveTop();
|
||||
if(exposed && cell.hasWater() && (cell.waterY()!=water || cell.featureId()!=feature))return null;
|
||||
int floor=exposed?cell.bedY():top;
|
||||
boolean wet=exposed && cell.hasWater();
|
||||
if(wet ? water-floor<1 || water-floor>4 : floor<water || floor>water+4)return null;
|
||||
for(int y=floor-2;y<=floor;y++)if(!terrain.solid(x,y,z))return null;
|
||||
int minY=floor-(wet?2:3),maxY=Math.max(floor,water)+12;
|
||||
if(terrain.excluded(x,minY,maxY,z))return null;
|
||||
return new Column(x,z,floor,water,wet,minY,maxY,feature,false);
|
||||
}
|
||||
public static Plan combine(Plan surface,Plan underground) {
|
||||
var sites=new ArrayList<>(surface.sites);sites.addAll(underground.sites);
|
||||
var columns=new ArrayList<>(surface.allColumns);columns.addAll(underground.allColumns);
|
||||
var refusals=new LinkedHashMap<>(surface.refusals);
|
||||
underground.refusals.forEach((reason,count)->refusals.merge(reason,count,Integer::sum));
|
||||
return new Plan(sites,columns,surface.candidates+underground.candidates,refusals);
|
||||
}
|
||||
private record LowerCandidate(Terrain25Shape.Chamber basin,int x,int z,long rank) {}
|
||||
/** Existing accepted basins only. No height, water cell or cave is created by this search. */
|
||||
public static Plan createUnderground(long seed,List<Terrain25Shape.Chamber> basins,Terrain terrain) {
|
||||
var candidates=new ArrayList<LowerCandidate>();
|
||||
for(var basin:basins) {
|
||||
if(basin.kind()!=Terrain25Shape.Kind.LAKE)continue;
|
||||
int water=basin.fluidY();
|
||||
for(int x=(int)Math.floor(basin.x()-basin.rx());x<=basin.x()+basin.rx();x+=2)
|
||||
for(int z=(int)Math.floor(basin.z()-basin.rz());z<=basin.z()+basin.rz();z+=2) {
|
||||
if(basin.cut(x,water,z)<=0)continue;
|
||||
boolean edge=false;
|
||||
for(int[] d:CARDINALS)if(basin.cut(x+d[0]*3,water,z+d[1]*3)<=0){edge=true;break;}
|
||||
if(edge)candidates.add(new LowerCandidate(basin,x,z,mix(seed^lowerFeature(basin)^key(x,z))));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingLong(LowerCandidate::rank).thenComparingInt(c->c.basin().id())
|
||||
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z));
|
||||
var sites=new ArrayList<Site>();var columns=new ArrayList<Column>();var refusals=new LinkedHashMap<String,Integer>();
|
||||
var caches=new HashMap<Integer,Map<Long,Optional<Column>>>();int attempts=0;
|
||||
for(var candidate:candidates) {
|
||||
if(attempts>=MAX_CANDIDATES||sites.size()>=MAX_UNDERGROUND_SITES)break;
|
||||
attempts++;
|
||||
var basin=candidate.basin();long feature=lowerFeature(basin);
|
||||
if(sites.stream().anyMatch(s->s.waterFeature()==feature)){refused(refusals,"lower-basin-already-represented");continue;}
|
||||
var cache=caches.computeIfAbsent(basin.id(),ignored->new HashMap<>());
|
||||
var centre=lowerColumn(candidate.x(),candidate.z(),basin,terrain,cache);
|
||||
if(centre==null||!centre.wet()){refused(refusals,"lower-no-water-clearance");continue;}
|
||||
var patch=new LinkedHashMap<Long,Column>();var visited=new HashMap<Long,Integer>();var queue=new ArrayDeque<Step>();
|
||||
for(int x=candidate.x()-SITE_RADIUS;x<=candidate.x()+SITE_RADIUS;x++)
|
||||
for(int z=candidate.z()-SITE_RADIUS;z<=candidate.z()+SITE_RADIUS;z++) {
|
||||
if(Math.hypot(x-candidate.x(),z-candidate.z())>SITE_RADIUS||basin.cut(x,basin.fluidY(),z)<=0)continue;
|
||||
var c=lowerColumn(x,z,basin,terrain,cache);
|
||||
if(c!=null&&c.wet()){queue.addLast(new Step(x,z,0));visited.put(key(x,z),0);}
|
||||
}
|
||||
while(!queue.isEmpty()) {
|
||||
var p=queue.removeFirst();var c=lowerColumn(p.x(),p.z(),basin,terrain,cache);if(c==null)continue;
|
||||
patch.put(key(p.x(),p.z()),c);if(p.distance()>=BANK_REACH)continue;
|
||||
for(int[] d:CARDINALS) {
|
||||
int x=p.x()+d[0],z=p.z()+d[1],distance=p.distance()+1;
|
||||
if(Math.hypot(x-candidate.x(),z-candidate.z())>SITE_RADIUS||visited.getOrDefault(key(x,z),Integer.MAX_VALUE)<=distance)continue;
|
||||
var next=lowerColumn(x,z,basin,terrain,cache);
|
||||
if(next==null||Math.abs(Math.max(c.floorY(),c.waterY())-Math.max(next.floorY(),next.waterY()))>1)continue;
|
||||
visited.put(key(x,z),distance);queue.addLast(new Step(x,z,distance));
|
||||
}
|
||||
}
|
||||
int wet=(int)patch.values().stream().filter(Column::wet).count(),banks=patch.size()-wet;
|
||||
if(wet<12||banks<32){refused(refusals,"lower-insufficient-walkable-shore");continue;}
|
||||
int minX=Integer.MAX_VALUE,minZ=Integer.MAX_VALUE,maxX=Integer.MIN_VALUE,maxZ=Integer.MIN_VALUE;
|
||||
for(var c:patch.values()){minX=Math.min(minX,c.x());minZ=Math.min(minZ,c.z());maxX=Math.max(maxX,c.x());maxZ=Math.max(maxZ,c.z());}
|
||||
sites.add(new Site(candidate.rank(),feature,candidate.x(),candidate.z(),basin.fluidY(),minX,minZ,maxX,maxZ,wet,banks,true));
|
||||
columns.addAll(patch.values());
|
||||
}
|
||||
return new Plan(sites,columns,attempts,refusals);
|
||||
}
|
||||
public static long lowerFeature(Terrain25Shape.Chamber basin) { return Long.MIN_VALUE+basin.id(); }
|
||||
private static Column lowerColumn(int x,int z,Terrain25Shape.Chamber basin,Terrain terrain,Map<Long,Optional<Column>> cache) {
|
||||
return cache.computeIfAbsent(key(x,z),ignored->Optional.ofNullable(lowerColumn(x,z,basin,terrain))).orElse(null);
|
||||
}
|
||||
private static Column lowerColumn(int x,int z,Terrain25Shape.Chamber basin,Terrain terrain) {
|
||||
int water=basin.fluidY();boolean wet=basin.cut(x,water,z)>0;
|
||||
// Read the floor in this waterline's band, never the island heightmap above the cave.
|
||||
int floor=-1;
|
||||
for(int y=water+4;y>=basin.floorY();y--)if(terrain.solid(x,y,z)&&!terrain.solid(x,y+1,z)){floor=y;break;}
|
||||
if(floor<0||(wet?(floor>=water||water-floor>6):(floor<water||floor>water+4)))return null;
|
||||
for(int y=floor-2;y<=floor;y++)if(!terrain.solid(x,y,z))return null;
|
||||
int base=Math.max(floor,water),maxY=base+12;
|
||||
for(int y=base+1;y<=maxY;y++)if(terrain.solid(x,y,z)){maxY=y-1;break;}
|
||||
if(maxY<base+4)return null;
|
||||
// A lower ledge may open on a flank; it must still sit below an existing higher rock mass.
|
||||
if(terrain.top(x,z)<maxY+8)return null;
|
||||
int minY=floor-(wet?2:3);
|
||||
if(terrain.excluded(x,minY,maxY,z))return null;
|
||||
return new Column(x,z,floor,water,wet,minY,maxY,lowerFeature(basin),true);
|
||||
}
|
||||
private static void refused(Map<String,Integer> reasons,String reason) { reasons.merge(reason,1,Integer::sum); }
|
||||
private static long key(int x,int z) { return ((long)x<<32)^(z&0xffffffffL); }
|
||||
private static long mix(long n) { n=(n^(n>>>30))*0xbf58476d1ce4e5b9L; n=(n^(n>>>27))*0x94d049bb133111ebL; return n^(n>>>31); }
|
||||
}
|
||||
@@ -23,12 +23,17 @@ public final class AerialPlanner24 {
|
||||
}
|
||||
|
||||
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
|
||||
if (generator.rootGeneration() != 24) throw new IllegalArgumentException("Aerial planning requires generation24");
|
||||
if ((generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) throw new IllegalArgumentException("Aerial planning requires generation24");
|
||||
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) return AerialPlan24.EMPTY;
|
||||
generator.prepareAerialSampling24(level);
|
||||
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
|
||||
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
|
||||
@Override public boolean reserved(int x, int z, int minY, int maxY) {
|
||||
if (generator.rootGeneration() == 25) {
|
||||
var binding = generator.starterContext();
|
||||
if (fr.koka.sanctuary.worldgen.Terrain25Runtime.protects(binding.generator(), binding.random(),
|
||||
new net.minecraft.core.BlockPos(x, minY, z), 0, maxY - minY, 0)) return true;
|
||||
}
|
||||
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,12 +35,22 @@ public final class AerialStore24 {
|
||||
public record Stored(String token, AerialPlan24 plan) {}
|
||||
|
||||
/** Store lives beside sanctuary-world-v24, so creating an orphan manifest never looks like an initialized journal. */
|
||||
public static Path directory(Path journalDirectory) { return journalDirectory.resolveSibling("sanctuary-aerial-v24"); }
|
||||
public static Path directory(Path journalDirectory) {
|
||||
return directory(journalDirectory, journalDirectory.getFileName().toString().equals("sanctuary-world-v25") ? 25 : 24);
|
||||
}
|
||||
public static Path directory(Path journalDirectory, int generation) {
|
||||
requireGeneration(generation);
|
||||
return journalDirectory.resolveSibling("sanctuary-aerial-v" + generation);
|
||||
}
|
||||
|
||||
public static Stored create(Path directory, long seed, int diameter, Supplier<AerialPlan24> factory) throws IOException {
|
||||
return create(directory, seed, diameter, 24, factory);
|
||||
}
|
||||
public static Stored create(Path directory, long seed, int diameter, int generation, Supplier<AerialPlan24> factory) throws IOException {
|
||||
requireGeneration(generation);
|
||||
AerialPlan24 plan = factory.get();
|
||||
validateExtent(plan, diameter);
|
||||
String encoded = encode(plan, seed, diameter).toString() + "\n";
|
||||
String encoded = encode(plan, seed, diameter, generation).toString() + "\n";
|
||||
byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length > MAX_BYTES) throw new IOException("Aerial layout exceeds its size limit");
|
||||
String token = digest(bytes);
|
||||
@@ -60,12 +70,16 @@ public final class AerialStore24 {
|
||||
}
|
||||
} finally { Files.deleteIfExists(temporary); }
|
||||
}
|
||||
Stored verified = load(directory, token, seed, diameter);
|
||||
Stored verified = load(directory, token, seed, diameter, generation);
|
||||
if (!verified.plan().sites().equals(plan.sites())) throw new IOException("Aerial manifest read-back differs from its plan");
|
||||
return verified;
|
||||
}
|
||||
|
||||
public static Stored load(Path directory, String token, long seed, int diameter) throws IOException {
|
||||
return load(directory, token, seed, diameter, 24);
|
||||
}
|
||||
public static Stored load(Path directory, String token, long seed, int diameter, int generation) throws IOException {
|
||||
requireGeneration(generation);
|
||||
if (token == null || !token.matches("[0-9a-f]{64}")) throw new IOException("Invalid aerial manifest token");
|
||||
Path file = directory.resolve(token + ".json");
|
||||
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|
||||
@@ -77,7 +91,7 @@ public final class AerialStore24 {
|
||||
JsonObject root = readJson(reader, 0).getAsJsonObject();
|
||||
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing aerial manifest data");
|
||||
fields(root, ROOT_FIELDS);
|
||||
if (integer(root, "schema") != 1 || integer(root, "generation") != 24
|
||||
if (integer(root, "schema") != 1 || integer(root, "generation") != generation
|
||||
|| number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != diameter)
|
||||
throw new IOException("Aerial manifest differs from its world contract");
|
||||
var sites = new ArrayList<AerialPlan24.Site>();
|
||||
@@ -103,9 +117,12 @@ public final class AerialStore24 {
|
||||
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
|
||||
}
|
||||
}
|
||||
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
|
||||
private static void requireGeneration(int generation) {
|
||||
if (generation != 24 && generation != 25) throw new IllegalArgumentException("Unsupported aerial world generation");
|
||||
}
|
||||
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter, int generation) {
|
||||
var root = new JsonObject();
|
||||
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", 24);
|
||||
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", generation);
|
||||
var sites = new JsonArray();
|
||||
for (var s : plan.sites()) {
|
||||
var site = new JsonObject(); site.addProperty("id", s.id()); site.addProperty("kind", s.kind().name());
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public final class AerialStructure24 extends Structure {
|
||||
}
|
||||
public String family() { return family; }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 24
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
|
||||
|| !generator.experimentalStructures()) return Optional.empty();
|
||||
var sites = generator.aerialPlan24().atStartChunk(context.chunkPos().x(), context.chunkPos().z()).stream()
|
||||
.filter(site -> AerialStructures24.family(site).equals(family)).toList();
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public final class AerialStructures24 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 24
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 24 && generator.rootGeneration() != 25)
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var sites = sanctuary.aerialPlan24().atStartChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (sites.isEmpty()) return;
|
||||
|
||||
@@ -29,18 +29,18 @@ public final class LostCityStructure extends Structure {
|
||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) && generator.cityReservation16(island) != null
|
||||
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) && generator.cityReservation16(island) != null
|
||||
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var support : generator.citySupports16(island)) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var support : generator.citySupports16(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||
}
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
@@ -50,13 +50,13 @@ public final class LostCityStructure extends Structure {
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ public final class LostCityUndergroundStructure22 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return Optional.empty();
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25))) return Optional.empty();
|
||||
var networks = generator.undergroundHall22().networks().stream()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
|
||||
+4
-4
@@ -41,9 +41,9 @@ public final class LostCityUndergroundStructures22 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) return;
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) return;
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25))) return;
|
||||
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
@@ -63,14 +63,14 @@ public final class LostCityUndergroundStructures22 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<LostCityUnderground16.Network>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.undergroundHall22().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)) || !island.status().equals("ready")) continue;
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
|
||||
+5
-5
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
|
||||
var level = source.getLevel();
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) {
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()) {
|
||||
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
||||
return 0;
|
||||
}
|
||||
@@ -28,20 +28,20 @@ public final class HeritageCommands19 {
|
||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var module : generator.transitPlan21().modules()) {
|
||||
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var module : generator.transitPlan21().modules()) {
|
||||
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
|
||||
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) for (var network : generator.undergroundHall22().networks()) {
|
||||
if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) for (var network : generator.undergroundHall22().networks()) {
|
||||
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty()) {
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !generator.mineralSanctuary23().isEmpty()) {
|
||||
var at = generator.mineralSanctuary23().chamber().locator();
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.mineral_sanctuary.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))
|
||||
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
|
||||
+ generator.undergroundHall22().networks().size() : 0);
|
||||
}))));
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
||||
var plan = generator.heritagePlan19();
|
||||
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
|
||||
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||
for (var segment : plan.segments())
|
||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
||||
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
||||
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
||||
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
||||
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
||||
if (pieces.isEmpty()) return Optional.empty();
|
||||
for (var piece : pieces) {
|
||||
var box = piece.getBoundingBox();
|
||||
|
||||
+2
-2
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
+15
-5
@@ -21,12 +21,16 @@ import net.minecraft.world.level.storage.loot.LootTable;
|
||||
/** Its complete immutable geometry is saved; a reload never consults the current planner. */
|
||||
public final class MineralSanctuaryPiece23 extends StructurePiece {
|
||||
private final MineralSanctuaryPlan23.Chamber chamber;
|
||||
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber) {
|
||||
private final int renderer;
|
||||
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber) { this(chamber, 23); }
|
||||
public MineralSanctuaryPiece23(MineralSanctuaryPlan23.Chamber chamber, int generation) {
|
||||
super(MineralSanctuaryStructures23.PIECE, 0, chamber.bounds());
|
||||
this.chamber = chamber; setOrientation(null);
|
||||
this.chamber = chamber; renderer = generation == 25 ? 25 : 23; setOrientation(null);
|
||||
}
|
||||
public MineralSanctuaryPiece23(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(MineralSanctuaryStructures23.PIECE, tag);
|
||||
renderer = tag.getInt("SanctuaryMineralRenderer").orElse(23);
|
||||
if (renderer != 23 && renderer != 25) throw new IllegalArgumentException("Unknown mineral renderer");
|
||||
chamber = decode(tag.getCompound("SanctuaryMineral").orElseThrow());
|
||||
if (!getBoundingBox().equals(chamber.bounds())) throw new IllegalArgumentException("Mineral piece bounding box mismatch");
|
||||
setOrientation(null);
|
||||
@@ -52,10 +56,11 @@ public final class MineralSanctuaryPiece23 extends StructurePiece {
|
||||
@Override public BlockPos getLocatorPosition() { return chamber.locator(); }
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryMineral", encode(chamber));
|
||||
if (renderer == 25) tag.putInt("SanctuaryMineralRenderer", renderer);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
MineralSanctuaryStructures23.placing(() -> MineralSanctuaryRenderer23.render(chamber, new LostCityRenderer.BlockWriter() {
|
||||
MineralSanctuaryStructures23.placing(() -> render(new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
var pos = new BlockPos(chamber.minX() + x, chamber.minY() + y, chamber.minZ() + z);
|
||||
if (boundingBox.isInside(pos) && clip.isInside(pos)) placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
|
||||
@@ -63,7 +68,12 @@ public final class MineralSanctuaryPiece23 extends StructurePiece {
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
throw new IllegalStateException("Mineral sanctuary contains natural veins, not loot containers");
|
||||
}
|
||||
}, clip.minX() - chamber.minX(), clip.minZ() - chamber.minZ(),
|
||||
clip.maxX() - chamber.minX(), clip.maxZ() - chamber.minZ()));
|
||||
}, clip));
|
||||
}
|
||||
private void render(LostCityRenderer.BlockWriter writer, BoundingBox clip) {
|
||||
int x0 = clip.minX() - chamber.minX(), z0 = clip.minZ() - chamber.minZ();
|
||||
int x1 = clip.maxX() - chamber.minX(), z1 = clip.maxZ() - chamber.minZ();
|
||||
if (renderer == 25) MineralSanctuaryRenderer25.render(chamber, writer, x0, z0, x1, z1);
|
||||
else MineralSanctuaryRenderer23.render(chamber, writer, x0, z0, x1, z1);
|
||||
}
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package fr.koka.sanctuary.worldgen.mineral;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Generation25 rock terraces with resolved corners and an inactive central blackstone frame. */
|
||||
public final class MineralSanctuaryRenderer25 {
|
||||
private MineralSanctuaryRenderer25() {}
|
||||
public static void render(MineralSanctuaryPlan23.Chamber c, LostCityRenderer.BlockWriter out) {
|
||||
render(c, out, 0, 0, c.width() - 1, c.depth() - 1);
|
||||
}
|
||||
/** Native chunk pieces render only their own columns; the saved absolute geometry stays identical. */
|
||||
public static void render(MineralSanctuaryPlan23.Chamber c, LostCityRenderer.BlockWriter out,
|
||||
int minX, int minZ, int maxX, int maxZ) {
|
||||
for (int z = Math.max(0, minZ); z <= Math.min(c.depth() - 1, maxZ); z++)
|
||||
for (int x = Math.max(0, minX); x <= Math.min(c.width() - 1, maxX); x++) {
|
||||
int floor = c.floor(x, z), ceiling = c.ceiling(x, z);
|
||||
double radius = c.radius(x, z);
|
||||
boolean inside = radius <= 1, access = c.accessFootprint(x, z), pillar = c.pillar(x, z);
|
||||
boolean fissure = c.portal(x, floor + 1, z);
|
||||
for (int y = 0; y < c.height(); y++) {
|
||||
boolean opening = fissure && y > floor && y <= floor + 5;
|
||||
if (!inside && !opening && !(access && y <= floor)) continue;
|
||||
boolean rock = y <= floor || y >= ceiling || radius > .95 || pillar;
|
||||
if (opening) rock = false;
|
||||
BlockState state = rock ? rock(c, x, y, z, floor, radius, pillar) : Blocks.AIR.defaultBlockState();
|
||||
if (rock && y == floor) {
|
||||
BlockState stair = stairAt(c, x, z);
|
||||
if (stair != null) state = stair.setValue(StairBlock.SHAPE, shape(c, x, z, stair));
|
||||
}
|
||||
int lateral = c.facing() % 2 == 0 ? x - c.width() / 2 : z - c.depth() / 2;
|
||||
int normal = c.facing() % 2 == 0 ? z - c.depth() / 2 : x - c.width() / 2;
|
||||
int bottom = c.floor(c.width() / 2, c.depth() / 2);
|
||||
if (normal == 0 && Math.abs(lateral) <= 3 && y >= bottom && y <= bottom + 8) {
|
||||
state = Math.abs(lateral) == 3 || y == bottom || y == bottom + 8
|
||||
? Blocks.BLACKSTONE.defaultBlockState() : Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
out.set(x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static BlockState stairAt(MineralSanctuaryPlan23.Chamber c, int x, int z) {
|
||||
if (x < 0 || x >= c.width() || z < 0 || z >= c.depth() || !c.footprint(x, z)
|
||||
|| c.centralFloor(x, z) || c.pillar(x, z)) return null;
|
||||
int floor = c.floor(x, z);
|
||||
BlockState material = rock(c, x, floor, z, floor, c.radius(x, z), false);
|
||||
if (!material.is(Blocks.STONE) && !material.is(Blocks.COBBLESTONE)) return null;
|
||||
for (Direction low : new Direction[]{Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}) {
|
||||
int nx = x + low.getStepX(), nz = z + low.getStepZ();
|
||||
if (nx >= 0 && nx < c.width() && nz >= 0 && nz < c.depth() && c.footprint(nx, nz)
|
||||
&& c.floor(nx, nz) == floor - 1) return
|
||||
(material.is(Blocks.COBBLESTONE) ? Blocks.COBBLESTONE_STAIRS : Blocks.STONE_STAIRS)
|
||||
.defaultBlockState().setValue(StairBlock.FACING, low.getOpposite());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static net.minecraft.world.level.block.state.properties.StairsShape shape(
|
||||
MineralSanctuaryPlan23.Chamber c, int x, int z, BlockState stair) {
|
||||
var facing = stair.getValue(StairBlock.FACING);
|
||||
for (boolean front : new boolean[]{true, false}) {
|
||||
Direction step = front ? facing : facing.getOpposite();
|
||||
int nx = x + step.getStepX(), nz = z + step.getStepZ();
|
||||
BlockState other = stairAt(c, nx, nz);
|
||||
if (other == null || c.floor(nx, nz) != c.floor(x, z)) continue;
|
||||
Direction turn = other.getValue(StairBlock.FACING);
|
||||
if (turn.getAxis() == facing.getAxis()) continue;
|
||||
Direction side = front ? turn.getOpposite() : turn;
|
||||
BlockState adjacent = stairAt(c, x + side.getStepX(), z + side.getStepZ());
|
||||
if (adjacent != null && c.floor(x + side.getStepX(), z + side.getStepZ()) == c.floor(x, z)
|
||||
&& adjacent.getValue(StairBlock.FACING) == facing) continue;
|
||||
boolean left = turn == facing.getCounterClockWise();
|
||||
return front
|
||||
? (left ? net.minecraft.world.level.block.state.properties.StairsShape.OUTER_LEFT : net.minecraft.world.level.block.state.properties.StairsShape.OUTER_RIGHT)
|
||||
: (left ? net.minecraft.world.level.block.state.properties.StairsShape.INNER_LEFT : net.minecraft.world.level.block.state.properties.StairsShape.INNER_RIGHT);
|
||||
}
|
||||
return net.minecraft.world.level.block.state.properties.StairsShape.STRAIGHT;
|
||||
}
|
||||
private static BlockState rock(MineralSanctuaryPlan23.Chamber c, int x, int y, int z, int floor, double radius, boolean pillar) {
|
||||
// Each ore follows a coherent curved seam. The exposed floor and columns reveal a seam
|
||||
// that continues into rock, instead of isolated reward blocks sprinkled through the air.
|
||||
double phase = (c.seed() & 1023) * .006;
|
||||
double seam = z + Math.sin(x * .065 + phase) * 4 + Math.sin(y * .19) * 2;
|
||||
boolean seamRock = y <= floor + 2 || radius > .90 && y <= floor + 12 || pillar;
|
||||
Block ore = null;
|
||||
if (seamRock && Math.abs(seam - c.depth() * .29) < 1.8) ore = Blocks.COAL_ORE;
|
||||
else if (seamRock && Math.abs(seam - c.depth() * .67) < 1.5) ore = Blocks.IRON_ORE;
|
||||
else if (seamRock && Math.abs(x + Math.sin(z * .09 + phase) * 3 - c.width() * .64) < 1.4) ore = Blocks.COPPER_ORE;
|
||||
if (ore != null) return ore.defaultBlockState();
|
||||
Block rare = rareVein(c, x, y, z);
|
||||
if (rare != null) return rare.defaultBlockState();
|
||||
long pattern = mix(c.seed() ^ ((long) (x / 4) << 40) ^ ((long) (z / 4) << 20) ^ y / 3);
|
||||
if (Math.floorMod(pattern, 13) < 2) return Blocks.COBBLESTONE.defaultBlockState();
|
||||
if (c.minY() + y < 100 && Math.floorMod(pattern, 5) == 0) return Blocks.DEEPSLATE.defaultBlockState();
|
||||
return switch (Math.floorMod(pattern, 29)) {
|
||||
case 0, 1 -> Blocks.ANDESITE.defaultBlockState();
|
||||
case 2 -> Blocks.DIORITE.defaultBlockState();
|
||||
case 3 -> Blocks.GRANITE.defaultBlockState();
|
||||
case 4, 5 -> Blocks.TUFF.defaultBlockState();
|
||||
default -> Blocks.STONE.defaultBlockState();
|
||||
};
|
||||
}
|
||||
/** Rare compact three-dimensional pockets depend on depth and seed, never a guaranteed reward. */
|
||||
private static Block rareVein(MineralSanctuaryPlan23.Chamber c, int x, int y, int z) {
|
||||
long roll = mix(c.seed() ^ 0x23DA7AL);
|
||||
if (c.minY() >= 160 || (roll & 1) == 0) return null;
|
||||
int vx = c.width() / 2 + (int) Math.floorMod(roll >>> 8, 37) - 18;
|
||||
int vz = c.depth() / 2 + ((roll & 4) == 0 ? -26 : 26);
|
||||
int vy = c.floor(vx, vz) - 1;
|
||||
double dx = x - vx, dy = y - vy, dz = z - vz;
|
||||
if (dx * dx / 5 + dy * dy / 3 + dz * dz / 4 > 1) return null;
|
||||
if (c.minY() < 96 && Math.floorMod(roll >>> 16, 6) == 0) return Blocks.DIAMOND_ORE;
|
||||
if (c.minY() < 128 && Math.floorMod(roll >>> 16, 3) == 0) return Blocks.REDSTONE_ORE;
|
||||
if (Math.floorMod(roll >>> 20, 7) == 0) return Blocks.EMERALD_ORE;
|
||||
return Blocks.GOLD_ORE;
|
||||
}
|
||||
private static long mix(long n) { n = (n ^ n >>> 30) * 0xbf58476d1ce4e5b9L; n = (n ^ n >>> 27) * 0x94d049bb133111ebL; return n ^ n >>> 31; }
|
||||
}
|
||||
+3
-3
@@ -11,14 +11,14 @@ public final class MineralSanctuaryStructure23 extends Structure {
|
||||
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return Optional.empty();
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) return Optional.empty();
|
||||
var plan = generator.mineralSanctuary23();
|
||||
if (plan.isEmpty()) return Optional.empty();
|
||||
var c = plan.chamber();
|
||||
if (c.startChunkX() != context.chunkPos().x() || c.startChunkZ() != context.chunkPos().z()) return Optional.empty();
|
||||
return Optional.of(new GenerationStub(c.locator(), builder -> builder.addPiece(new MineralSanctuaryPiece23(c))));
|
||||
return Optional.of(new GenerationStub(c.locator(), builder -> builder.addPiece(new MineralSanctuaryPiece23(c, generator.rootGeneration()))));
|
||||
}
|
||||
@Override public StructureType<?> type() { return MineralSanctuaryStructures23.TYPE; }
|
||||
}
|
||||
|
||||
+4
-4
@@ -40,9 +40,9 @@ public final class MineralSanctuaryStructures23 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !sanctuary.experimentalStructures()) return;
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return;
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)) return;
|
||||
var plan = sanctuary.mineralSanctuary23();
|
||||
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|
||||
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return;
|
||||
@@ -63,14 +63,14 @@ public final class MineralSanctuaryStructures23 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
|| (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var plan = generator.mineralSanctuary23();
|
||||
if (plan.isEmpty() || !generator.experimentalStructures()) return null;
|
||||
var c = plan.chamber();
|
||||
var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ());
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24) || !island.status().equals("ready")) return null;
|
||||
if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24 && island.generation() != 25) || !island.status().equals("ready")) return null;
|
||||
if (Math.abs((long) c.startChunkX() - (origin.getX() >> 4)) > radius
|
||||
|| Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null;
|
||||
var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
|
||||
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||
var networks = generator.ruinsPlan17().networks().stream()
|
||||
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
|
||||
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||
}));
|
||||
}
|
||||
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.experimentalStructures()
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
|
||||
public TransitStructure21(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) return Optional.empty();
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return Optional.empty();
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)))) return Optional.empty();
|
||||
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (modules.isEmpty()) return Optional.empty();
|
||||
for (var module : modules) {
|
||||
@@ -25,7 +25,7 @@ public final class TransitStructure21 extends Structure {
|
||||
throw new IllegalStateException("Transit module exceeds local native reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
||||
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24 || generator.rootGeneration() == 25))
|
||||
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
|
||||
? new TransitPiece22(module) : new TransitPiece21(module)))));
|
||||
}
|
||||
|
||||
+4
-4
@@ -46,10 +46,10 @@ public final class TransitStructures21 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !sanctuary.experimentalStructures()) return;
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)))
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) return;
|
||||
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24 && island.generation() != 25)))) return;
|
||||
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
||||
@@ -66,7 +66,7 @@ public final class TransitStructures21 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24 && generator.rootGeneration() != 25)) && !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryDecorationProtection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
|
||||
/** Initial25 mines protect the water beside their actual corridors, not unused gaps in the graph's enclosing square. */
|
||||
public final class MineHydrologyAdmission25 {
|
||||
public static final int MAX_PROBES = 1_000_000;
|
||||
private final PopulationGenerationContext.Binding binding;
|
||||
private final Map<Long, SanctuaryDecorationProtection> masks = new HashMap<>();
|
||||
public MineHydrologyAdmission25(PopulationGenerationContext.Binding binding) { this.binding = binding; }
|
||||
public record Check(String rejection, BlockPos at, int probes) {}
|
||||
public Check check(StructureStart start) {
|
||||
return firstProtected(start.getPieces().stream().map(piece -> envelope(piece.getBoundingBox())).toList(), pos -> {
|
||||
int cx = pos.getX() >> 4, cz = pos.getZ() >> 4;
|
||||
long key = ((long) cx << 32) ^ (cz & 0xffffffffL);
|
||||
return masks.computeIfAbsent(key, ignored -> SanctuaryDecorationProtection.create(binding, cx, cz)).protects(pos);
|
||||
});
|
||||
}
|
||||
/** Owned native supports are clipped to -12/+4; the native liquid-shell probe extends one further block. */
|
||||
public static BoundingBox envelope(BoundingBox piece) {
|
||||
var writes = UndergroundPlacement25.writeBounds("minecraft:mineshaft", piece);
|
||||
return new BoundingBox(writes.minX() - 1, Math.max(0, writes.minY() - 1), writes.minZ() - 1,
|
||||
writes.maxX() + 1, Math.min(383, writes.maxY() + 1), writes.maxZ() + 1);
|
||||
}
|
||||
public static Check firstProtected(List<BoundingBox> volumes, Predicate<BlockPos> protectedAt) {
|
||||
int probes = 0;
|
||||
var at = new BlockPos.MutableBlockPos();
|
||||
for (var box : volumes) for (int x = box.minX(); x <= box.maxX(); x++)
|
||||
for (int z = box.minZ(); z <= box.maxZ(); z++) for (int y = box.minY(); y <= box.maxY(); y++) {
|
||||
if (++probes > MAX_PROBES) return new Check("mine25:hydrology-probe-budget", at.immutable(), probes - 1);
|
||||
at.set(x, y, z);
|
||||
if (protectedAt.test(at)) return new Check("mine25:protected-hydrology", at.immutable(), probes);
|
||||
}
|
||||
return new Check(null, null, probes);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
|
||||
/** Vanilla dungeon placement at generation25 altitudes, independently of the optional Sanctuary secrets. */
|
||||
public final class MonsterRoom25Feature implements Feature {
|
||||
public static final MapCodec<MonsterRoom25Feature> CODEC = MapCodec.unit(MonsterRoom25Feature::new);
|
||||
@Override public MapCodec<MonsterRoom25Feature> codec() { return CODEC; }
|
||||
@Override public boolean place(WorldGenLevel level, ChunkGenerator ignored, RandomSource random, BlockPos origin) {
|
||||
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.getLevel().dimension())
|
||||
|| !level.getLevel().structureManager().shouldGenerateStructures()) return false;
|
||||
var island = generator.islandAt(origin.getX(), origin.getZ());
|
||||
if (island == null || !island.origin()) return false;
|
||||
long seed = random.nextLong();
|
||||
var dimensions = RandomSource.create(seed);
|
||||
int rx = dimensions.nextInt(2) + 3, rz = dimensions.nextInt(2) + 3;
|
||||
var box = new BoundingBox(origin.getX() - rx, origin.getY() - 1, origin.getZ() - rz,
|
||||
origin.getX() + rx, origin.getY() + 4, origin.getZ() + rz);
|
||||
if (box.minY() < 1 || box.maxY() >= level.getMaxY()
|
||||
|| !island.contains(box.minX(), box.minZ()) || !island.contains(box.maxX(), box.maxZ())
|
||||
|| generator.intersectsExperimentalStructures(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ())) return false;
|
||||
var noise = generator.delegate(island); var state = generator.randomState(island);
|
||||
int openings = 0;
|
||||
// Refuse the complete placement before the first write: a protected wall must not produce half a dungeon.
|
||||
for (int z = box.minZ(); z <= box.maxZ(); z++) for (int x = box.minX(); x <= box.maxX(); x++) {
|
||||
if (!level.getBlockState(new BlockPos(x, box.minY(), z)).isSolid()
|
||||
|| !level.getBlockState(new BlockPos(x, box.maxY(), z)).isSolid()) return false;
|
||||
for (int y = box.minY(); y <= box.maxY(); y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
if (!level.getBlockState(at).getFluidState().isEmpty() || generator.protectsCityDecoration(at)
|
||||
|| Terrain25Runtime.protects(noise, state, at, 1, 1, 1)
|
||||
|| !level.ensureCanWrite(at)) return false;
|
||||
}
|
||||
if ((x == box.minX() || x == box.maxX() || z == box.minZ() || z == box.maxZ())
|
||||
&& level.isEmptyBlock(new BlockPos(x, origin.getY(), z))
|
||||
&& level.isEmptyBlock(new BlockPos(x, origin.getY() + 1, z))) openings++;
|
||||
}
|
||||
if (openings < 1 || openings > 5) return false;
|
||||
final boolean[] placed = { false };
|
||||
UndergroundPlacement25.placingBounds(box,
|
||||
() -> placed[0] = new MonsterRoomFeature().place(level, generator, RandomSource.create(seed), origin));
|
||||
return placed[0];
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25FallProtection;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** A dry native hut may stand beside shallow still water; its four piers must stop on an intact bed. */
|
||||
public final class SwampHutAdmission25 {
|
||||
public static final int MAX_WATER_DEPTH = 4;
|
||||
public static final int MAX_PIER_DROP = 8;
|
||||
@FunctionalInterface public interface Terrain { BlockState at(int x, int y, int z); }
|
||||
private SwampHutAdmission25() {}
|
||||
|
||||
public static String rejection(SanctuaryChunkGenerator host, ExpansionIsland island, StructureStart start,
|
||||
BiFunction<Integer, Integer, NoiseColumn> columns) {
|
||||
return rejection(host, island, start, columns, false);
|
||||
}
|
||||
public static String rejection(SanctuaryChunkGenerator host, ExpansionIsland island, StructureStart start,
|
||||
BiFunction<Integer, Integer, NoiseColumn> columns, boolean lowerWetland) {
|
||||
var generator = host.delegate(island); var random = host.randomState(island);
|
||||
for (var piece : start.getPieces()) {
|
||||
if (!(piece instanceof SwampHutPiece)) return "hut25:unexpected-piece";
|
||||
var body = piece.getBoundingBox();
|
||||
var volume = new BoundingBox(body.minX(), body.minY() - MAX_PIER_DROP - 2, body.minZ(), body.maxX(), body.maxY(), body.maxZ());
|
||||
// Query nearby source regions too: their actual waterfalls may reach this footprint.
|
||||
for (var region : PopulationHydrologyRuntime.plansIntersecting(generator, random,
|
||||
body.minX() - 80, body.minZ() - 80, body.maxX() + 80, body.maxZ() + 80)) {
|
||||
String hazard = legacyHazard(region, volume);
|
||||
if (hazard != null) return hazard;
|
||||
for (var fall : Terrain25Runtime.falls(generator, random, region.region()))
|
||||
if (Terrain25FallProtection.intersects(fall, volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ()))
|
||||
return "hut25:waterfall25";
|
||||
}
|
||||
for (var basin : Terrain25Runtime.admittedLakes(generator, random)) {
|
||||
if (volume.maxY() < basin.floorY() - 2 || volume.minY() > basin.fluidY() + 2) continue;
|
||||
for (int x = volume.minX() - 2; x <= volume.maxX() + 2; x++) for (int z = volume.minZ() - 2; z <= volume.maxZ() + 2; z++)
|
||||
if (basin.horizontal(x, z) < 1.3) {
|
||||
if (!lowerWetland) return "hut25:deep-basin25";
|
||||
if (basin.kind() != fr.koka.sanctuary.worldgen.Terrain25Shape.Kind.LAKE) return "hut25:protected-lava-basin25";
|
||||
// The native body may not cut a reservoir's retaining wall below its waterline.
|
||||
// Only the four piers can extend below it; geometryRejection preserves their sealed beds.
|
||||
if (body.minY() < basin.fluidY()) return "hut25:reservoir-waterline-cut";
|
||||
}
|
||||
}
|
||||
String geometry = geometryRejection(piece, (x, y, z) -> columns.apply(x, z).getBlock(y));
|
||||
if (geometry != null) return geometry;
|
||||
if (lowerWetland) {
|
||||
String headroom = headroomRejection(piece, (x, y, z) -> columns.apply(x, z).getBlock(y));
|
||||
if (headroom != null) return headroom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Same source/flow/lava margins as the existing hydrology reservation; only still-water cells are exempted. */
|
||||
public static String legacyHazard(PopulationHydrologyRuntime.RegionPlan region, BoundingBox volume) {
|
||||
var water = region.water(); var lava = region.lava();
|
||||
String flowing = flowHazard(water.springs(), water.spills(), volume);
|
||||
if (flowing != null) return flowing;
|
||||
for (var bounds : lava.flowBounds()) if (intersects(volume, bounds, 8)) return "hut25:protected-lava-flow";
|
||||
for (var cell : lava.cells()) if (near(volume, cell.x(), cell.y(), cell.z(), 2)) return "hut25:protected-lava-cell";
|
||||
for (var support : lava.supports()) if (near(volume, support.x(), support.y(), support.z(), 2)) return "hut25:protected-lava-support";
|
||||
var rivers = water.features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.RIVER).map(PopulationHydrology.Feature::id)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
for (int x = volume.minX(); x <= volume.maxX(); x++) for (int z = volume.minZ(); z <= volume.maxZ(); z++)
|
||||
for (var cell : water.cellsAt(x, z)) if (rivers.contains(cell.featureId())
|
||||
&& volume.minY() <= cell.carveTop() && volume.maxY() >= cell.bedY() - cell.sedimentDepth() - 1)
|
||||
return "hut25:river-channel";
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String flowHazard(List<PopulationHydrology.Spring> springs, List<PopulationHydrology.Spill> spills, BoundingBox volume) {
|
||||
for (var spring : springs) {
|
||||
if (intersects(volume, spring.flowBounds(), 2) || near(volume, spring.source().x(), spring.source().y(), spring.source().z(), 3))
|
||||
return "hut25:protected-spring";
|
||||
}
|
||||
for (var spill : spills)
|
||||
if (intersects(volume, spill.flowBounds(), 3) || near(volume, spill.source().x(), spill.source().y(), spill.source().z(), 3))
|
||||
return "hut25:protected-spill";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Vanilla scattered huts average MOTION_BLOCKING_NO_LEAVES, which includes a still-water surface. */
|
||||
public static int localNativeHeight(NoiseColumn column, int minY, int localCeiling) {
|
||||
var opaque = net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING_NO_LEAVES.isOpaque();
|
||||
for (int y = localCeiling; y >= minY; y--) if (opaque.test(column.getBlock(y))) return y + 1;
|
||||
return minY;
|
||||
}
|
||||
public static String geometryRejection(StructurePiece piece, Terrain terrain) {
|
||||
var box = piece.getBoundingBox();
|
||||
if (!(piece instanceof SwampHutPiece) || piece.getOrientation() == null) return "hut25:unexpected-piece";
|
||||
int deckY = box.minY() + 1;
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
|
||||
int waterTop = Integer.MIN_VALUE, waterBottom = Integer.MAX_VALUE;
|
||||
for (int y = box.minY() - MAX_PIER_DROP; y <= box.maxY(); y++) {
|
||||
var fluid = terrain.at(x, y, z).getFluidState();
|
||||
if (fluid.isEmpty()) continue;
|
||||
// This contract admits vanilla water only; it must not depend on datapack tags being loaded.
|
||||
if (fluid.getType() != Fluids.WATER && fluid.getType() != Fluids.FLOWING_WATER) return "hut25:non-water-fluid";
|
||||
if (!fluid.isSource()) return "hut25:flowing-water";
|
||||
waterTop = Math.max(waterTop, y); waterBottom = Math.min(waterBottom, y);
|
||||
}
|
||||
if (waterTop == Integer.MIN_VALUE) continue;
|
||||
if (waterTop > deckY) return "hut25:flooded-interior";
|
||||
if (waterTop - waterBottom + 1 > MAX_WATER_DEPTH) return "hut25:deep-water";
|
||||
int bed = waterBottom - 1;
|
||||
// Native wall/log writes begin at local Y0. They must not cut a retaining bed.
|
||||
if (bed >= box.minY() || !solid(terrain.at(x, bed, z)) || !solid(terrain.at(x, bed - 1, z))) return "hut25:unsealed-water-bed";
|
||||
}
|
||||
for (var pier : piers(piece)) {
|
||||
int foot = pier.getY(), replaced = 0;
|
||||
while (replaceable(terrain.at(pier.getX(), foot, pier.getZ()))) {
|
||||
if (replaced++ >= MAX_PIER_DROP) return "hut25:pier-too-deep";
|
||||
foot--;
|
||||
}
|
||||
if (!solid(terrain.at(pier.getX(), foot, pier.getZ())) || !solid(terrain.at(pier.getX(), foot - 1, pier.getZ())))
|
||||
return "hut25:unsealed-pier-foot";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A lower hut occupies existing cave air; the native renderer must not excavate its own ceiling. */
|
||||
public static String headroomRejection(StructurePiece piece, Terrain terrain) {
|
||||
var box = piece.getBoundingBox();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
for (int y = box.minY() + 2; y <= box.maxY(); y++)
|
||||
if (!terrain.at(x, y, z).isAir()) return "hut25:insufficient-natural-headroom";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** SwampHutPiece.postProcess uses exactly X1/5, Z2/7 at local Y-1. */
|
||||
public static List<BlockPos> piers(StructurePiece piece) {
|
||||
var result = new ArrayList<BlockPos>(4);
|
||||
for (int z : new int[]{2, 7}) for (int x : new int[]{1, 5}) result.add(worldPosition(piece, x, -1, z));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
public static BlockPos worldPosition(StructurePiece piece, int x, int y, int z) {
|
||||
var b = piece.getBoundingBox(); Direction facing = piece.getOrientation();
|
||||
if (facing == null || facing.getAxis().isVertical()) throw new IllegalArgumentException("A native hut needs horizontal orientation");
|
||||
int wx = switch (facing) { case WEST -> b.maxX() - z; case EAST -> b.minX() + z; default -> b.minX() + x; };
|
||||
int wz = switch (facing) { case NORTH -> b.maxZ() - z; case SOUTH -> b.minZ() + z; default -> b.minZ() + x; };
|
||||
return new BlockPos(wx, b.minY() + y, wz);
|
||||
}
|
||||
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
|
||||
/** Exact stopping predicate of StructurePiece.fillColumnDown in Minecraft26.3-pre-2. */
|
||||
private static boolean replaceable(BlockState state) {
|
||||
return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS);
|
||||
}
|
||||
private static boolean near(BoundingBox b, int x, int y, int z, int margin) {
|
||||
return b.minX() <= x + margin && b.maxX() >= x - margin && b.minY() <= y + margin && b.maxY() >= y - margin
|
||||
&& b.minZ() <= z + margin && b.maxZ() >= z - margin;
|
||||
}
|
||||
private static boolean intersects(BoundingBox b, PopulationHydrology.FlowBounds water, int margin) {
|
||||
return b.minX() <= water.maxX() + margin && b.maxX() >= water.minX() - margin
|
||||
&& b.minY() <= water.maxY() + margin && b.maxY() >= water.minY() - margin
|
||||
&& b.minZ() <= water.maxZ() + margin && b.maxZ() >= water.minZ() - margin;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
|
||||
/** Pure geometry shared by actual placement and shape/support diagnostics. */
|
||||
public final class UndergroundGeometry25 {
|
||||
private UndergroundGeometry25() {}
|
||||
@FunctionalInterface public interface Solid { boolean at(int x, int y, int z); }
|
||||
public static int naturalVaultY(int x, int z, int firstY, int limitY, Solid solid) {
|
||||
for (int y = firstY; y <= limitY; y++) if (solid.at(x, y, z) && solid.at(x, y + 1, z)) return y;
|
||||
return -1;
|
||||
}
|
||||
public static boolean outsideOccupied(BoundingBox passage, List<BoundingBox> occupied) {
|
||||
return occupied.stream().noneMatch(volume -> volume.intersects(passage));
|
||||
}
|
||||
public static int roofAt(BoundingBox box, int x, int z, boolean access) {
|
||||
if (x < box.minX() || x > box.maxX() || z < box.minZ() || z > box.maxZ()) return -1;
|
||||
if (box.getXSpan() <= 7 || box.getZSpan() <= 7 || access) return box.maxY();
|
||||
double dx = Math.max(0, Math.max(box.minX() + 5 - x, x - (box.maxX() - 5))) / 5.0;
|
||||
double dz = Math.max(0, Math.max(box.minZ() + 5 - z, z - (box.maxZ() - 5))) / 5.0;
|
||||
double edge = dx * dx + dz * dz;
|
||||
return edge > 1 ? -1 : box.maxY() - (int) Math.round(edge * 3);
|
||||
}
|
||||
public record Cabin(BoundingBox bounds, int floor, BlockPos entrance) {
|
||||
public Cabin {
|
||||
if (bounds.getXSpan() != 9 || bounds.getZSpan() != 9 || bounds.minY() != floor - 1 || bounds.maxY() != floor + 6)
|
||||
throw new IllegalArgumentException("Miner cabin requires its complete 9x8x9 envelope");
|
||||
entrance = entrance.immutable();
|
||||
int x = entrance.getX() - bounds.minX(), z = entrance.getZ() - bounds.minZ();
|
||||
if (entrance.getY() != floor || !((x == 4 && (z == 1 || z == 7)) || (z == 4 && (x == 1 || x == 7))))
|
||||
throw new IllegalArgumentException("Miner entrance is not a supported door position");
|
||||
}
|
||||
public Rotation rotation() {
|
||||
int x = entrance.getX() - bounds.minX(), z = entrance.getZ() - bounds.minZ();
|
||||
return z == 1 ? Rotation.NONE : x == 7 ? Rotation.CLOCKWISE_90 : z == 7 ? Rotation.CLOCKWISE_180 : Rotation.COUNTERCLOCKWISE_90;
|
||||
}
|
||||
public BlockPos position(int x, int y, int z) {
|
||||
int rx = x, rz = z;
|
||||
switch (rotation()) {
|
||||
case CLOCKWISE_90 -> { rx = 8 - z; rz = x; }
|
||||
case CLOCKWISE_180 -> { rx = 8 - x; rz = 8 - z; }
|
||||
case COUNTERCLOCKWISE_90 -> { rx = z; rz = 8 - x; }
|
||||
default -> {}
|
||||
}
|
||||
return new BlockPos(bounds.minX() + rx, y, bounds.minZ() + rz);
|
||||
}
|
||||
public BlockPos chest() { return position(6, floor, 3); }
|
||||
public void render(BiConsumer<BlockPos, BlockState> writer) {
|
||||
int y0 = floor - 1;
|
||||
for (int z = 0; z < 9; z++) for (int x = 0; x < 9; x++) for (int y = 0; y <= 7; y++) {
|
||||
BlockState state = Blocks.CAVE_AIR.defaultBlockState();
|
||||
if (y == 0) state = Blocks.COBBLESTONE.defaultBlockState();
|
||||
if (y >= 1 && y <= 4 && x >= 1 && x <= 7 && z >= 1 && z <= 7 && (x == 1 || x == 7 || z == 1 || z == 7))
|
||||
state = (y == 1 ? Blocks.STONE_BRICKS : Blocks.OAK_PLANKS).defaultBlockState();
|
||||
if (y >= 1 && y <= 4 && (x == 1 || x == 7) && (z == 1 || z == 7)) state = Blocks.STRIPPED_OAK_LOG.defaultBlockState();
|
||||
if (y == 4 && x >= 1 && x <= 7 && z >= 1 && z <= 7 && (x == 1 || x == 7 || z == 1 || z == 7)) state = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
int roof = 4 + Math.min(x, 8 - x);
|
||||
if (roof <= 7 && y == roof) state = Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, x < 4 ? Direction.EAST : Direction.WEST);
|
||||
if ((z == 1 || z == 7) && x >= 2 && x <= 6 && y >= 4 && y < roof) state = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
if (x == 4 && y == 7) state = Blocks.OAK_PLANKS.defaultBlockState();
|
||||
if ((x == 1 || x == 7) && z >= 3 && z <= 4 && y == 3) state = Blocks.GLASS.defaultBlockState();
|
||||
if (x == 4 && z == 1 && (y == 1 || y == 2)) state = Blocks.OAK_DOOR.defaultBlockState()
|
||||
.setValue(DoorBlock.FACING, Direction.NORTH).setValue(DoorBlock.HALF, y == 1 ? DoubleBlockHalf.LOWER : DoubleBlockHalf.UPPER);
|
||||
put(writer, x, y0 + y, z, state);
|
||||
}
|
||||
put(writer, 2, floor, 6, Blocks.CRAFTING_TABLE.defaultBlockState());
|
||||
put(writer, 3, floor, 6, Blocks.FURNACE.defaultBlockState());
|
||||
put(writer, 6, floor, 6, Blocks.BARREL.defaultBlockState());
|
||||
put(writer, 6, floor + 1, 6, Blocks.LANTERN.defaultBlockState());
|
||||
put(writer, 2, floor, 3, Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING, Direction.WEST));
|
||||
// The runtime creates this chest separately to attach native loot exactly once.
|
||||
}
|
||||
private void put(BiConsumer<BlockPos, BlockState> writer, int x, int y, int z, BlockState state) {
|
||||
writer.accept(position(x, y, z), state.rotate(rotation()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
/** Saved cavity/raccord and the small miner shelter. Village buildings and mines remain native pieces. */
|
||||
public final class UndergroundPiece25 extends StructurePiece {
|
||||
public enum Kind { VILLAGE_CAVERN, ACCESS, MINER_CABIN, MONSTER_ROOM }
|
||||
private final Kind kind;
|
||||
private final int floorY;
|
||||
private final long seed;
|
||||
private final BlockPos entrance;
|
||||
private final List<BoundingBox> volumes;
|
||||
private boolean featureAttempted;
|
||||
private boolean featurePlaced;
|
||||
|
||||
public UndergroundPiece25(Kind kind, int floorY, long seed, BlockPos entrance, List<BoundingBox> volumes) {
|
||||
super(UndergroundStructures25.PIECE, 0, bounds(volumes));
|
||||
this.kind = kind; this.floorY = floorY; this.seed = seed; this.entrance = entrance.immutable();
|
||||
this.volumes = List.copyOf(volumes); setOrientation(null); validate();
|
||||
}
|
||||
public UndergroundPiece25(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(UndergroundStructures25.PIECE, tag);
|
||||
if (tag.getIntOr("Schema", 0) != 1) throw new IllegalArgumentException("Unknown underground25 piece schema");
|
||||
kind = Kind.valueOf(tag.getString("Kind").orElseThrow()); floorY = tag.getInt("Floor").orElseThrow();
|
||||
seed = tag.getLong("Seed").orElseThrow();
|
||||
entrance = new BlockPos(tag.getInt("EntryX").orElseThrow(), tag.getInt("EntryY").orElseThrow(), tag.getInt("EntryZ").orElseThrow());
|
||||
List<BoundingBox> decoded = new ArrayList<>();
|
||||
ListTag list = tag.getList("Volumes").orElseThrow();
|
||||
if (list.isEmpty() || list.size() > 512) throw new IllegalArgumentException("Invalid underground25 volume count");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
var t = list.getCompound(i).orElseThrow();
|
||||
decoded.add(new BoundingBox(t.getInt("x0").orElseThrow(), t.getInt("y0").orElseThrow(), t.getInt("z0").orElseThrow(),
|
||||
t.getInt("x1").orElseThrow(), t.getInt("y1").orElseThrow(), t.getInt("z1").orElseThrow()));
|
||||
}
|
||||
volumes = List.copyOf(decoded);
|
||||
featureAttempted = tag.getBooleanOr("FeatureAttempted", false);
|
||||
featurePlaced = tag.getBooleanOr("FeaturePlaced", false);
|
||||
setOrientation(null); validate();
|
||||
if (!getBoundingBox().equals(bounds(volumes))) throw new IllegalArgumentException("Underground25 piece bounds mismatch");
|
||||
}
|
||||
private void validate() {
|
||||
if (floorY < 3 || floorY > 345 || volumes.isEmpty() || volumes.size() > 512
|
||||
|| getBoundingBox().getXSpan() > 256 || getBoundingBox().getZSpan() > 256
|
||||
|| getBoundingBox().minY() < 1 || getBoundingBox().maxY() > 358)
|
||||
throw new IllegalArgumentException("Invalid underground25 geometry");
|
||||
if (kind == Kind.MINER_CABIN) cabinShape();
|
||||
if (featurePlaced && (!featureAttempted || kind != Kind.MONSTER_ROOM))
|
||||
throw new IllegalArgumentException("Invalid underground25 feature state");
|
||||
}
|
||||
public static BoundingBox bounds(List<BoundingBox> volumes) {
|
||||
return BoundingBox.encapsulatingBoxes(volumes).orElseThrow();
|
||||
}
|
||||
public Kind kind() { return kind; }
|
||||
public int floorY() { return floorY; }
|
||||
public long seed() { return seed; }
|
||||
public List<BoundingBox> volumes() { return volumes; }
|
||||
public boolean featureAttempted() { return featureAttempted; }
|
||||
public boolean featurePlaced() { return featurePlaced; }
|
||||
@Override public BlockPos getLocatorPosition() { return entrance; }
|
||||
public boolean projects(int x, int z) {
|
||||
return kind == Kind.VILLAGE_CAVERN && volumes.stream().anyMatch(b -> x >= b.minX() && x <= b.maxX() && z >= b.minZ() && z <= b.maxZ());
|
||||
}
|
||||
public BlockState terrainOverride(int x, int y, int z) {
|
||||
if (kind != Kind.VILLAGE_CAVERN && kind != Kind.ACCESS) return null;
|
||||
if (!getBoundingBox().isInside(x, y, z)) return null;
|
||||
int roof = -1;
|
||||
for (var volume : volumes) roof = Math.max(roof, UndergroundGeometry25.roofAt(volume, x, z, kind == Kind.ACCESS));
|
||||
if (roof < floorY + 2 || y < floorY - 2 || y > roof) return null;
|
||||
if (y == floorY - 2) return Blocks.STONE.defaultBlockState();
|
||||
if (y == floorY - 1) return (kind == Kind.ACCESS ? Blocks.STONE : Blocks.DIRT).defaultBlockState();
|
||||
return Blocks.CAVE_AIR.defaultBlockState();
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.putInt("Schema", 1); tag.putString("Kind", kind.name()); tag.putInt("Floor", floorY); tag.putLong("Seed", seed);
|
||||
tag.putInt("EntryX", entrance.getX()); tag.putInt("EntryY", entrance.getY()); tag.putInt("EntryZ", entrance.getZ());
|
||||
ListTag list = new ListTag();
|
||||
for (var b : volumes) {
|
||||
var t = new CompoundTag(); t.putInt("x0", b.minX()); t.putInt("y0", b.minY()); t.putInt("z0", b.minZ());
|
||||
t.putInt("x1", b.maxX()); t.putInt("y1", b.maxY()); t.putInt("z1", b.maxZ()); list.add(t);
|
||||
}
|
||||
tag.put("Volumes", list); tag.putBoolean("FeatureAttempted", featureAttempted); tag.putBoolean("FeaturePlaced", featurePlaced);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
if (kind == Kind.MONSTER_ROOM) { monsterRoom(level, generator, clip); return; }
|
||||
if (kind == Kind.MINER_CABIN) { cabin(level, clip); return; }
|
||||
var b = getBoundingBox();
|
||||
for (int z = Math.max(b.minZ(), clip.minZ()); z <= Math.min(b.maxZ(), clip.maxZ()); z++)
|
||||
for (int x = Math.max(b.minX(), clip.minX()); x <= Math.min(b.maxX(), clip.maxX()); x++)
|
||||
for (int y = b.minY(); y <= b.maxY(); y++) {
|
||||
var block = terrainOverride(x, y, z);
|
||||
if (block != null) placeBlock(level, block, x, y, z, clip);
|
||||
}
|
||||
}
|
||||
private synchronized void monsterRoom(WorldGenLevel level, ChunkGenerator generator, BoundingBox clip) {
|
||||
if (featureAttempted || !clip.isInside(entrance)) return;
|
||||
// The exact vanilla feature owns its opening checks, one spawner, chest placement and mob distribution.
|
||||
featureAttempted = true;
|
||||
featurePlaced = new MonsterRoomFeature().place(level, generator, RandomSource.create(seed), entrance);
|
||||
}
|
||||
public UndergroundGeometry25.Cabin cabinShape() { return new UndergroundGeometry25.Cabin(getBoundingBox(), floorY, entrance); }
|
||||
private void cabin(WorldGenLevel level, BoundingBox clip) {
|
||||
cabinShape().render((pos, state) -> placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip));
|
||||
createChest(level, clip, RandomSource.create(seed), chestPosition(), UndergroundStructures25.MINER_LOOT, null);
|
||||
}
|
||||
public Rotation cabinRotation() { return cabinShape().rotation(); }
|
||||
public BlockPos cabinPosition(int x, int y, int z) { return cabinShape().position(x, y, z); }
|
||||
public BlockPos chestPosition() { return cabinShape().chest(); }
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
|
||||
/** Scope comes from saved pieces, so roads use the same underground floor after a partial-world reload. */
|
||||
public final class UndergroundPlacement25 {
|
||||
public record Context(List<BoundingBox> bounds, UndergroundPiece25 cavern) {}
|
||||
private static final ThreadLocal<Context> CURRENT = new ThreadLocal<>();
|
||||
private UndergroundPlacement25() {}
|
||||
public static boolean isPlacing() { return CURRENT.get() != null; }
|
||||
public static boolean mayWrite(BlockPos pos) {
|
||||
var context = CURRENT.get();
|
||||
return context != null && context.bounds().stream().anyMatch(box -> box.isInside(pos));
|
||||
}
|
||||
public static Integer projectedHeight(int x, int z) {
|
||||
var context = CURRENT.get();
|
||||
if (context == null || context.cavern() == null || !context.cavern().projects(x, z)) return null;
|
||||
return context.cavern().floorY();
|
||||
}
|
||||
public static void placing(WorldGenLevel level, StructureStart start, Runnable action) {
|
||||
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.getLevel().dimension())) {
|
||||
action.run(); return;
|
||||
}
|
||||
var structures = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
// Compare the persisted native identity and start chunk, never the in-memory preview object.
|
||||
// This also recognizes a partially generated mineshaft or hut loaded from its own NBT.
|
||||
var planned = UndergroundStructures25.plan(generator).starts().stream().filter(candidate ->
|
||||
candidate.start().getChunkPos().equals(start.getChunkPos())
|
||||
&& structures.getOrThrow(candidate.key()).value() == start.getStructure()).findFirst().orElse(null);
|
||||
if (planned == null) { action.run(); return; }
|
||||
UndergroundPiece25 cavern = null;
|
||||
for (var piece : start.getPieces()) if (piece instanceof UndergroundPiece25 extra) {
|
||||
if (extra.kind() == UndergroundPiece25.Kind.VILLAGE_CAVERN) cavern = extra;
|
||||
}
|
||||
var previous = CURRENT.get();
|
||||
CURRENT.set(new Context(start.getPieces().stream().map(p -> writeBounds(planned.key().identifier().toString(), p.getBoundingBox())).toList(), cavern));
|
||||
try { action.run(); } finally { if (previous == null) CURRENT.remove(); else CURRENT.set(previous); }
|
||||
}
|
||||
/** Native supports use the same vertical margin that previewAt already certified against other structures. */
|
||||
public static BoundingBox writeBounds(String id, BoundingBox box) {
|
||||
if (!id.equals("minecraft:mineshaft") && !id.equals("minecraft:swamp_hut")) return box;
|
||||
return new BoundingBox(box.minX(), Math.max(1, box.minY() - 12), box.minZ(),
|
||||
box.maxX(), Math.min(358, box.maxY() + 4), box.maxZ());
|
||||
}
|
||||
public static void placingRoom(UndergroundPiece25 room, Runnable action) {
|
||||
placingBounds(room.getBoundingBox(), action);
|
||||
}
|
||||
public static void placingBounds(BoundingBox bounds, Runnable action) {
|
||||
var previous = CURRENT.get(); CURRENT.set(new Context(List.of(bounds), null));
|
||||
try { action.run(); } finally { if (previous == null) CURRENT.remove(); else CURRENT.set(previous); }
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
|
||||
/** Immutable admission result. Native pieces themselves acquire their normal one-shot placement flags. */
|
||||
public record UndergroundPlan25(List<PlannedStart> starts, List<UndergroundPiece25> rooms, Map<String, Integer> diagnostics, int columns) {
|
||||
public static final UndergroundPlan25 EMPTY = new UndergroundPlan25(List.of(), List.of(), Map.of(), 0);
|
||||
public UndergroundPlan25 { starts = List.copyOf(starts); rooms = List.copyOf(rooms); diagnostics = Map.copyOf(diagnostics); }
|
||||
public record PlannedStart(ResourceKey<Structure> key, StructureStart start, BlockPos entrance) {
|
||||
public PlannedStart { entrance = entrance.immutable(); }
|
||||
}
|
||||
public boolean intersects(BoundingBox box) {
|
||||
for (var planned : starts) for (var piece : planned.start().getPieces())
|
||||
if (UndergroundPlacement25.writeBounds(planned.key().identifier().toString(), piece.getBoundingBox()).intersects(box)) return true;
|
||||
for (var room : rooms) if (room.getBoundingBox().intersects(box)) return true;
|
||||
return false;
|
||||
}
|
||||
public boolean protects(BlockPos pos) {
|
||||
for (var planned : starts) for (var piece : planned.start().getPieces())
|
||||
if (UndergroundPlacement25.writeBounds(planned.key().identifier().toString(), piece.getBoundingBox()).isInside(pos)) return true;
|
||||
for (var room : rooms) if (room.getBoundingBox().isInside(pos)) return true;
|
||||
return false;
|
||||
}
|
||||
/** Shared with final terrain sampling. A null value leaves natural terrain untouched. */
|
||||
public BlockState terrainOverride(int x, int y, int z) {
|
||||
for (var planned : starts) for (var piece : planned.start().getPieces())
|
||||
if (piece instanceof UndergroundPiece25 extra) {
|
||||
BlockState state = extra.terrainOverride(x, y, z);
|
||||
if (state != null) return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public boolean carves(int x, int y, int z) {
|
||||
var state = terrainOverride(x, y, z);
|
||||
return state != null && state.isAir();
|
||||
}
|
||||
}
|
||||
+544
@@ -0,0 +1,544 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Runtime;
|
||||
import fr.koka.sanctuary.worldgen.Terrain25Shape;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
|
||||
/** Bounded read-only admission over final natural terrain. Large native graphs are refused, never truncated. */
|
||||
public final class UndergroundPlanner25 {
|
||||
public static final int MAX_COLUMNS = 16_000;
|
||||
public static final int MAX_VILLAGE_ATTEMPTS = 18;
|
||||
public static final int MAX_CENTRAL_VILLAGE_CANDIDATES = 16;
|
||||
public static final int MAX_MINE_ATTEMPTS = 16;
|
||||
private UndergroundPlanner25() {}
|
||||
public static UndergroundPlan25 plan(ServerLevel level, SanctuaryChunkGenerator generator) {
|
||||
var planner = new Planner(level, generator);
|
||||
try { planner.village(); } catch (BudgetExceeded ex) { planner.refuse("village:terrain-budget"); }
|
||||
planner.diagnostics.put("village:sampled-columns", planner.terrain.columns.size());
|
||||
planner.nextStage();
|
||||
try { planner.mine(); } catch (BudgetExceeded ex) { planner.refuse("mine:terrain-budget"); }
|
||||
planner.diagnostics.put("mine:sampled-columns", planner.terrain.columns.size());
|
||||
planner.nextStage();
|
||||
try { planner.hut(); } catch (BudgetExceeded ex) { planner.refuse("hut:terrain-budget"); }
|
||||
planner.diagnostics.put("hut:sampled-columns", planner.terrain.columns.size());
|
||||
planner.nextStage();
|
||||
try { planner.rooms(); } catch (BudgetExceeded ex) { planner.refuse("rooms:terrain-budget"); }
|
||||
planner.diagnostics.put("rooms:sampled-columns", planner.terrain.columns.size());
|
||||
return new UndergroundPlan25(planner.accepted, planner.roomFeatures, planner.diagnostics, planner.columnsUsed + planner.terrain.columns.size());
|
||||
}
|
||||
/** Same native candidate pool and budget, spread around the island instead of stopping in the northern rows. */
|
||||
public static List<ExpansionStructures23.Candidate> mineCandidates(ExpansionIsland island) {
|
||||
var all = ExpansionStructures23.candidates(island);
|
||||
if (all.isEmpty()) return List.of();
|
||||
var centre = all.getFirst();
|
||||
var quadrants = new ArrayList<List<ExpansionStructures23.Candidate>>(4);
|
||||
for (int quadrant = 0; quadrant < 4; quadrant++) quadrants.add(new ArrayList<>());
|
||||
for (var candidate : all) if (!candidate.chunk().equals(centre.chunk()))
|
||||
quadrants.get(mineQuadrant(island, candidate.chunk())).add(candidate);
|
||||
Comparator<ExpansionStructures23.Candidate> nearestFirst = Comparator
|
||||
.comparingDouble((ExpansionStructures23.Candidate c) -> {
|
||||
double dx = (double) c.chunk().getMiddleBlockX() - island.centerX();
|
||||
double dz = (double) c.chunk().getMiddleBlockZ() - island.centerZ();
|
||||
return dx * dx + dz * dz;
|
||||
}).thenComparingInt(c -> c.chunk().x()).thenComparingInt(c -> c.chunk().z());
|
||||
quadrants.forEach(list -> list.sort(nearestFirst));
|
||||
var selected = new ArrayList<ExpansionStructures23.Candidate>(); selected.add(centre);
|
||||
for (int depth = 0; selected.size() < Math.min(MAX_MINE_ATTEMPTS, all.size()); depth++)
|
||||
for (var quadrant : quadrants) if (depth < quadrant.size() && selected.size() < MAX_MINE_ATTEMPTS)
|
||||
selected.add(quadrant.get(depth));
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
public static int mineQuadrant(ExpansionIsland island, ChunkPos chunk) {
|
||||
return (chunk.getMiddleBlockX() < island.centerX() ? 0 : 1)
|
||||
+ (chunk.getMiddleBlockZ() < island.centerZ() ? 0 : 2);
|
||||
}
|
||||
private static final class BudgetExceeded extends RuntimeException {}
|
||||
private static final class Terrain {
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionIsland root;
|
||||
final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
Terrain(ServerLevel level, SanctuaryChunkGenerator generator, ExpansionIsland root) { this.level = level; this.generator = generator; this.root = root; }
|
||||
NoiseColumn column(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var result = columns.get(key);
|
||||
if (result == null) {
|
||||
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
|
||||
result = generator.getBaseColumn(x, z, level, generator.randomState(root)); columns.put(key, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
BlockState at(int x, int y, int z) { return column(x, z).getBlock(y); }
|
||||
boolean solid(int x, int y, int z) { var state = at(x, y, z); return state.isSolid() && state.getFluidState().isEmpty(); }
|
||||
boolean air(int x, int y, int z) { return at(x, y, z).isAir(); }
|
||||
boolean protectedAt(int x, int y, int z) {
|
||||
return !at(x, y, z).getFluidState().isEmpty()
|
||||
|| Terrain25Runtime.protects(generator.delegate(root), generator.randomState(root), new BlockPos(x, y, z), 1, 3, 2);
|
||||
}
|
||||
int top(int x, int z) {
|
||||
var col = column(x, z);
|
||||
for (int y = 358; y > 0; y--) if (!col.getBlock(y).isAir()) return y;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
private static final class Planner {
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionIsland root;
|
||||
Terrain terrain;
|
||||
int columnsUsed;
|
||||
final Terrain25Shape.Plan shape;
|
||||
final List<UndergroundPlan25.PlannedStart> accepted = new ArrayList<>();
|
||||
final List<UndergroundPiece25> roomFeatures = new ArrayList<>();
|
||||
final Map<String, Integer> diagnostics = new LinkedHashMap<>();
|
||||
final long deadline;
|
||||
Planner(ServerLevel level, SanctuaryChunkGenerator generator) {
|
||||
this.level = level; this.generator = generator;
|
||||
root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
terrain = new Terrain(level, generator, root);
|
||||
shape = Terrain25Runtime.shape(generator.delegate(root), generator.randomState(root));
|
||||
deadline = Long.MAX_VALUE;
|
||||
}
|
||||
void refuse(String reason) { diagnostics.merge(reason, 1, Integer::sum); }
|
||||
void checkBudget() { /* Bounds are counts, never elapsed time: identical results on every host. */ }
|
||||
void nextStage() { columnsUsed += terrain.columns.size(); terrain = new Terrain(level, generator, root); }
|
||||
boolean conflict(BoundingBox b) {
|
||||
if (b.minY() < 3 || b.maxY() > 357 || !root.contains(b.minX(), b.minZ()) || !root.contains(b.maxX(), b.maxZ())) return true;
|
||||
if (generator.intersectsExperimentalStructures(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())) return true;
|
||||
for (var a : accepted) for (var p : a.start().getPieces())
|
||||
if (UndergroundPlacement25.writeBounds(a.key().identifier().toString(), p.getBoundingBox()).intersects(b)) return true;
|
||||
for (var room : roomFeatures) if (room.getBoundingBox().intersects(b)) return true;
|
||||
return false;
|
||||
}
|
||||
Holder.Reference<Structure> holder(ResourceKey<Structure> key) { return level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(key); }
|
||||
void add(ResourceKey<Structure> key, StructureStart start, BlockPos entrance) {
|
||||
accepted.add(new UndergroundPlan25.PlannedStart(key, start, entrance)); refuse("accepted:" + key.identifier());
|
||||
}
|
||||
void village() {
|
||||
int attempts = 0;
|
||||
var chambers = shape.chambers().stream().filter(c -> c.kind() == Terrain25Shape.Kind.DRY)
|
||||
.sorted(Comparator.comparingDouble((Terrain25Shape.Chamber c) -> c.rx() * c.rz()).reversed()).toList();
|
||||
for (var chamber : chambers) {
|
||||
List<ChunkPos> candidates;
|
||||
if (chamber == chambers.getFirst()) candidates = centralVillageChunks(chamber);
|
||||
else {
|
||||
int x = (int) Math.round(chamber.x()), z = (int) Math.round(chamber.z());
|
||||
candidates = List.of(new ChunkPos(x >> 4, z >> 4), new ChunkPos((x + 16) >> 4, z >> 4), new ChunkPos((x - 16) >> 4, z >> 4));
|
||||
}
|
||||
for (var chunk : candidates) {
|
||||
if (attempts++ >= MAX_VILLAGE_ATTEMPTS) { refuse("village:attempt-budget"); return; }
|
||||
refuse("village:candidates-attempted");
|
||||
checkBudget();
|
||||
int floor = chamber.floorY() + 1;
|
||||
StructureStart nativeStart = villageStart(chunk, floor);
|
||||
if (!nativeStart.isValid()) { refuse("village:native-refused"); continue; }
|
||||
long houses = nativeStart.getPieces().stream().filter(p -> p instanceof PoolElementStructurePiece pool
|
||||
&& pool.getElement().toString().contains("/houses/")).count();
|
||||
long streetCount = nativeStart.getPieces().stream().filter(p -> p instanceof PoolElementStructurePiece pool
|
||||
&& pool.getElement().toString().contains("/streets/")).count();
|
||||
SanctuaryMod.LOGGER.info("Underground25 village candidate chamber {} fit {} centre {},{},{} radii {},{},{} start {} floor {} graph {} houses {} streets {} bounds {}",
|
||||
chamber.id(), shape.dryFits().get(chamber.id()), chamber.x(), chamber.y(), chamber.z(), chamber.rx(), chamber.ry(), chamber.rz(),
|
||||
chunk, floor, nativeStart.getPieces().size(), houses, streetCount, nativeStart.getBoundingBox());
|
||||
if (houses < 6) { refuse("village:not-extended"); continue; }
|
||||
boolean deepConflict = false;
|
||||
BlockPos deepAt = null; String deepReason = null;
|
||||
deepPieces:
|
||||
for (var piece : nativeStart.getPieces()) {
|
||||
var box = piece.getBoundingBox();
|
||||
if (conflict(box)) { deepConflict = true; deepAt = box.getCenter(); deepReason = "reserved-volume"; break; }
|
||||
// Wells and other native sub-pieces can extend below the common road floor.
|
||||
if (box.minY() < floor - 2) for (int pz = box.minZ(); pz <= box.maxZ(); pz += 2)
|
||||
for (int px = box.minX(); px <= box.maxX(); px += 2) {
|
||||
if (!terrain.solid(px, box.minY() - 1, pz)) {
|
||||
deepConflict = true; deepAt = new BlockPos(px, box.minY() - 1, pz); deepReason = "missing-deep-support"; break deepPieces;
|
||||
}
|
||||
for (int py = box.minY() - 1; py < floor - 2; py++)
|
||||
if (terrain.protectedAt(px, py, pz)) {
|
||||
deepConflict = true; deepAt = new BlockPos(px, py, pz); deepReason = "protected-deep-volume"; break deepPieces;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deepConflict) {
|
||||
SanctuaryMod.LOGGER.info("Underground25 village chamber {} start {} floor {} refused {} at {}", chamber.id(), chunk, floor, deepReason, deepAt);
|
||||
refuse("village:native-deep-piece-conflict"); continue;
|
||||
}
|
||||
List<BoundingBox> volumes = new ArrayList<>();
|
||||
for (var p : nativeStart.getPieces()) {
|
||||
var b = p.getBoundingBox();
|
||||
volumes.add(new BoundingBox(b.minX() - 6, floor - 2, b.minZ() - 6,
|
||||
b.maxX() + 6, Math.max(floor + 10, b.maxY() + 5), b.maxZ() + 6));
|
||||
}
|
||||
String rejection = certifyVillage(volumes, floor, chamber);
|
||||
if (rejection != null) { refuse("village:" + rejection); continue; }
|
||||
var streets = nativeStart.getPieces().stream().filter(p -> p instanceof PoolElementStructurePiece pool
|
||||
&& pool.getElement().toString().contains("/streets/")).toList();
|
||||
var buildings = nativeStart.getPieces().stream().filter(p -> !streets.contains(p)).toList();
|
||||
var access = access(streets, floor, volumes, buildings, "village");
|
||||
if (access == null) { refuse("village:no-natural-access"); continue; }
|
||||
volumes.addAll(access.volumes());
|
||||
var complete = UndergroundPiece25.bounds(volumes);
|
||||
if (complete.minX() < chunk.getMinBlockX() - 128 || complete.maxX() > chunk.getMaxBlockX() + 128
|
||||
|| complete.minZ() < chunk.getMinBlockZ() - 128 || complete.maxZ() > chunk.getMaxBlockZ() + 128
|
||||
|| complete.getXSpan() > 256 || complete.getZSpan() > 256) {
|
||||
refuse("village:native-reference-range"); continue;
|
||||
}
|
||||
var cavity = new UndergroundPiece25(UndergroundPiece25.Kind.VILLAGE_CAVERN, floor,
|
||||
mix(root.seed() ^ chunk.pack()), access.entrance(), volumes);
|
||||
List<StructurePiece> pieces = new ArrayList<>(); pieces.add(cavity); pieces.addAll(nativeStart.getPieces());
|
||||
var start = new StructureStart(holder(UndergroundStructures25.VILLAGE).value(), chunk, 0, new PiecesContainer(pieces));
|
||||
add(UndergroundStructures25.VILLAGE, start, access.entrance()); return;
|
||||
}
|
||||
}
|
||||
refuse("village:no-fitting-cavern");
|
||||
}
|
||||
StructureStart villageStart(ChunkPos chunk, int floor) {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
Structure original = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getValue(Identifier.withDefaultNamespace("village_plains"));
|
||||
JsonObject json = Structure.DIRECT_CODEC.encodeStart(ops, original).getOrThrow().getAsJsonObject().deepCopy();
|
||||
json.addProperty("start_pool", "sanctuary:underground25/zombie_plains_starts");
|
||||
json.addProperty("size", 6); json.addProperty("max_distance_from_center", 80);
|
||||
json.addProperty("terrain_adaptation", "none"); json.remove("project_start_to_heightmap");
|
||||
var height = new JsonObject(); height.addProperty("absolute", floor); json.add("start_height", height);
|
||||
var structure = Structure.DIRECT_CODEC.parse(ops, json).getOrThrow();
|
||||
var random = generator.randomState(root);
|
||||
var view = new FloorView(generator, level, floor);
|
||||
var proto = new ProtoChunk(chunk, UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||
var start = structure.generate(holder(UndergroundStructures25.VILLAGE), level.dimension(), level.registryAccess(), view,
|
||||
generator.getBiomeSource(), random.createClimateSampler(SamplerContext.builder().enableCaches().build()), random,
|
||||
level.getStructureTemplateManager(), root.seed(), chunk, 0, proto, b -> !b.is(Biomes.DEEP_DARK));
|
||||
if (!start.isValid()) return start;
|
||||
return new StructureStart(holder(UndergroundStructures25.VILLAGE).value(), chunk, 0, new PiecesContainer(start.getPieces()));
|
||||
}
|
||||
String certifyVillage(List<BoundingBox> volumes, int floor, Terrain25Shape.Chamber chamber) {
|
||||
int count = 0, naturallyOpen = 0, roof = 0;
|
||||
int roofLow = Integer.MAX_VALUE, roofHigh = Integer.MIN_VALUE;
|
||||
int roofLimit = Math.min(356, (int) Math.ceil(chamber.y() + chamber.ry()) + 5);
|
||||
SanctuaryMod.LOGGER.info("Underground25 village chamber {}: floor {}, vault search limit {}", chamber.id(), floor, roofLimit);
|
||||
for (var b : volumes) {
|
||||
checkBudget(); if (conflict(b)) return "protected-or-outside";
|
||||
for (int z = b.minZ() + 3; z <= b.maxZ() - 2; z += 4) for (int x = b.minX() + 3; x <= b.maxX() - 2; x += 4) {
|
||||
if (!terrain.solid(x, floor - 1, z) || !terrain.solid(x, floor - 2, z)) {
|
||||
boolean inFootprint = chamber.cut(x, chamber.floorY() + 1, z) > 0;
|
||||
var membrane = shape.dryFloor(x, floor - 2, z);
|
||||
refuse("village:unsupported-" + (inFootprint ? "inside" : "outside") + "-chamber-floor");
|
||||
var density = Terrain25Runtime.view(generator.delegate(root), generator.randomState(root));
|
||||
SanctuaryMod.LOGGER.info("Underground25 village floor {} unsupported at {},{}: surface {}, below {}, chamber {} horizontal {}, in floor footprint {}, certified membrane {}, natural density {}/{}, excavated density {}/{}",
|
||||
floor, x, z, terrain.at(x, floor - 1, z), terrain.at(x, floor - 2, z), chamber.id(), chamber.horizontal(x, z), inFootprint,
|
||||
membrane == null ? -1 : membrane.id(),
|
||||
density.natural(SamplerContext.EMPTY_UNCACHED, x, floor - 1, z), density.natural(SamplerContext.EMPTY_UNCACHED, x, floor - 2, z),
|
||||
density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor - 1, z), density.sampleValue(SamplerContext.EMPTY_UNCACHED, x, floor - 2, z));
|
||||
return "unsupported-floor";
|
||||
}
|
||||
count++; if (terrain.air(x, floor + 2, z) && terrain.air(x, floor + 6, z)) naturallyOpen++;
|
||||
// A tall natural cavern need not put its ceiling immediately above a low house or road.
|
||||
// Search only within this cavern's known height envelope; retain actual rock, never manufacture a roof.
|
||||
int vaultY = UndergroundGeometry25.naturalVaultY(x, z, b.maxY() + 1, roofLimit, terrain::solid);
|
||||
if (vaultY >= 0) { roof++; roofLow = Math.min(roofLow, vaultY); roofHigh = Math.max(roofHigh, vaultY); }
|
||||
for (int y = floor - 1; y <= b.maxY() + 2; y += 3) if (terrain.protectedAt(x, y, z)) {
|
||||
SanctuaryMod.LOGGER.info("Underground25 village chamber {} protected water at {},{},{} block {} volume {} after {} supported columns",
|
||||
chamber.id(), x, y, z, terrain.at(x, y, z), b, count);
|
||||
return "protected-water";
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics.merge("village:vault-sampled-columns", count, Integer::sum);
|
||||
diagnostics.merge("village:vault-covered-columns", roof, Integer::sum);
|
||||
if (roof > 0) {
|
||||
diagnostics.merge("village:vault-lowest-y", roofLow, Math::min);
|
||||
diagnostics.merge("village:vault-highest-y", roofHigh, Math::max);
|
||||
}
|
||||
SanctuaryMod.LOGGER.info("Underground25 village chamber {}: floor {}, {} sampled columns, {} naturally open, {} covered, roof Y{}..{} (limit {})",
|
||||
chamber.id(), floor, count, naturallyOpen, roof, roof == 0 ? -1 : roofLow, roof == 0 ? -1 : roofHigh, roofLimit);
|
||||
if (count == 0 || naturallyOpen * 100 < count * 60) return "cavern-too-small";
|
||||
if (roof * 100 < count * 55) return "insufficient-rock-cover";
|
||||
return null;
|
||||
}
|
||||
record Access(BlockPos entrance, List<BoundingBox> volumes, int dx, int dz) {}
|
||||
void accessRefusal(String purpose, String reason, BlockPos at) {
|
||||
String key = purpose + ":access-" + reason;
|
||||
if (!diagnostics.containsKey(key)) SanctuaryMod.LOGGER.info("Underground25 {} access first {} at {}", purpose, reason, at);
|
||||
refuse(key);
|
||||
}
|
||||
Access access(List<StructurePiece> pieces, int floor, List<BoundingBox> occupied, List<StructurePiece> obstacles, String purpose) {
|
||||
// Begin at real rooms/streets; a route is accepted only when it reaches supported natural cave air.
|
||||
var origins = pieces.stream().sorted(Comparator.comparingInt(p -> p.getBoundingBox().getXSpan() * p.getBoundingBox().getZSpan())).limit(18).toList();
|
||||
for (var piece : origins) for (int facing = 0; facing < 4; facing++) {
|
||||
checkBudget(); var b = piece.getBoundingBox();
|
||||
int dx = facing == 0 ? 1 : facing == 2 ? -1 : 0, dz = facing == 1 ? 1 : facing == 3 ? -1 : 0;
|
||||
int x0 = dx > 0 ? b.maxX() : dx < 0 ? b.minX() : b.getCenter().getX();
|
||||
int z0 = dz > 0 ? b.maxZ() : dz < 0 ? b.minZ() : b.getCenter().getZ();
|
||||
List<BoundingBox> route = new ArrayList<>(); int open = 0, freed = 0;
|
||||
boolean interrupted = false;
|
||||
for (int step = 0; step <= 40; step++) {
|
||||
int x = x0 + dx * step, z = z0 + dz * step;
|
||||
var slice = new BoundingBox(x - Math.abs(dz), floor - 2, z - Math.abs(dx), x + Math.abs(dz), floor + 3, z + Math.abs(dx));
|
||||
boolean outside = UndergroundGeometry25.outsideOccupied(slice, occupied);
|
||||
if (outside && occupied.stream().anyMatch(v -> x >= v.minX() && x <= v.maxX() && z >= v.minZ() && z <= v.maxZ())) freed++;
|
||||
if (outside && conflict(slice)) {
|
||||
accessRefusal(purpose, "protected-volume", new BlockPos(x, floor, z)); interrupted = true; break;
|
||||
}
|
||||
if (obstacles.stream().anyMatch(p -> p.getBoundingBox().intersects(slice))) {
|
||||
accessRefusal(purpose, "building", new BlockPos(x, floor, z)); interrupted = true; break;
|
||||
}
|
||||
boolean pass = true, natural = true;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int sx = x - dz * side, sz = z + dx * side;
|
||||
if (!terrain.solid(sx, floor - 2, sz)) {
|
||||
accessRefusal(purpose, "missing-support", new BlockPos(sx, floor - 2, sz)); pass = false; break;
|
||||
}
|
||||
for (int y = floor - 1; y <= floor + 3; y++) if (terrain.protectedAt(sx, y, sz)) {
|
||||
accessRefusal(purpose, "protected-water", new BlockPos(sx, y, sz)); pass = false; break;
|
||||
}
|
||||
if (!pass) break;
|
||||
natural &= terrain.solid(sx, floor - 1, sz) && terrain.air(sx, floor, sz) && terrain.air(sx, floor + 1, sz);
|
||||
}
|
||||
if (!pass) { interrupted = true; break; }
|
||||
route.add(slice); open = outside && natural ? open + 1 : 0;
|
||||
if (open >= 3 && Math.hypot(x, z) >= 48) {
|
||||
diagnostics.merge(purpose + ":access-shadow-freed-columns", freed, Integer::sum);
|
||||
SanctuaryMod.LOGGER.info("Underground25 {} access: floor {}, origin {}, endpoint {}, {} columns, {} freed from other floors' XZ shadows",
|
||||
purpose, floor, new BlockPos(x0, floor, z0), new BlockPos(x, floor, z), route.size(), freed);
|
||||
return new Access(new BlockPos(x, floor, z), List.copyOf(route), dx, dz);
|
||||
}
|
||||
}
|
||||
diagnostics.merge(purpose + ":access-shadow-freed-columns", freed, Integer::sum);
|
||||
if (!interrupted) accessRefusal(purpose, "no-natural-opening-in-range", new BlockPos(x0 + dx * 40, floor, z0 + dz * 40));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
void mine() {
|
||||
var candidates = mineCandidates(root);
|
||||
diagnostics.put("mine:candidates-available", ExpansionStructures23.candidates(root).size());
|
||||
diagnostics.put("mine:candidates-selected", candidates.size());
|
||||
for (var candidate : candidates) {
|
||||
checkBudget(); refuse("mine:candidates-attempted");
|
||||
refuse("mine:quadrant-" + mineQuadrant(root, candidate.chunk()));
|
||||
var nativeMine = ExpansionStructures23.previewMine25(level, generator, root, candidate, deadline);
|
||||
if (nativeMine == null) { refuse("mine:native-refused"); continue; }
|
||||
if (nativeMine.start().getPieces().stream().anyMatch(p -> conflict(p.getBoundingBox()))) { refuse("mine:protected"); continue; }
|
||||
var minePieces = nativeMine.start().getPieces();
|
||||
// The mine itself is useful even if no safe attached shelter can be found.
|
||||
var key = nativeMine.structure().unwrapKey().orElseThrow();
|
||||
UndergroundPiece25 cabin = null, approach = null;
|
||||
// Prefer corridors already close to a natural chamber instead of whichever pieces the recursive native graph emitted first.
|
||||
for (var piece : minePieces.stream().filter(p -> p.getBoundingBox().getYSpan() <= 4)
|
||||
.sorted(Comparator.comparingDouble(this::caveDistance)).limit(32).toList()) {
|
||||
refuse("cabin:corridors-considered");
|
||||
int floor = piece.getBoundingBox().minY();
|
||||
var access = access(List.of(piece), floor, minePieces.stream().map(StructurePiece::getBoundingBox).toList(), List.of(), "cabin");
|
||||
if (access == null) continue;
|
||||
refuse("cabin:accessible-corridors");
|
||||
int x = access.entrance().getX(), z = access.entrance().getZ();
|
||||
int ox = x - (access.dx() < 0 ? 7 : access.dx() > 0 ? 1 : 4);
|
||||
int oz = z - (access.dz() < 0 ? 7 : access.dz() > 0 ? 1 : 4);
|
||||
var box = new BoundingBox(ox, floor - 1, oz, ox + 8, floor + 6, oz + 8);
|
||||
if (conflict(box)) { refuse("cabin:reserved-box"); continue; }
|
||||
if (minePieces.stream().anyMatch(p -> p.getBoundingBox().intersects(box))) { refuse("cabin:mine-box-intersection"); continue; }
|
||||
boolean supported = true; int open = 0, cells = 0;
|
||||
BlockPos missingFloor = null, protectedCell = null;
|
||||
for (int bz = box.minZ(); bz <= box.maxZ(); bz += 2) for (int bx = box.minX(); bx <= box.maxX(); bx += 2) {
|
||||
boolean support = terrain.solid(bx, floor - 1, bz);
|
||||
supported &= support;
|
||||
if (!support && missingFloor == null) missingFloor = new BlockPos(bx, floor - 1, bz);
|
||||
for (int y = floor; y <= floor + 6; y++) {
|
||||
cells++; if (terrain.air(bx, y, bz)) open++;
|
||||
if (terrain.protectedAt(bx, y, bz)) { supported = false; if (protectedCell == null) protectedCell = new BlockPos(bx, y, bz); }
|
||||
}
|
||||
}
|
||||
diagnostics.merge("cabin:best-box-open-percent", open * 100 / cells, Math::max);
|
||||
if (!supported || open * 100 < cells * 65) {
|
||||
if (missingFloor != null) refuse("cabin:box-missing-floor");
|
||||
if (protectedCell != null) refuse("cabin:box-protected-water");
|
||||
if (open * 100 < cells * 65) refuse("cabin:box-not-open");
|
||||
SanctuaryMod.LOGGER.info("Underground25 cabin box {} floor {} missing floor {}, protected cell {}, open {}/{}",
|
||||
box, floor, missingFloor, protectedCell, open, cells);
|
||||
continue;
|
||||
}
|
||||
cabin = new UndergroundPiece25(UndergroundPiece25.Kind.MINER_CABIN, floor, mix(root.seed() ^ candidate.roll()),
|
||||
new BlockPos(x, floor, z), List.of(box));
|
||||
approach = new UndergroundPiece25(UndergroundPiece25.Kind.ACCESS, floor, root.seed(), access.entrance(), access.volumes());
|
||||
break;
|
||||
}
|
||||
add(key, nativeMine.start(), nativeMine.start().getPieces().getFirst().getLocatorPosition());
|
||||
if (cabin != null) {
|
||||
var position = new ChunkPos(cabin.getLocatorPosition().getX() >> 4, cabin.getLocatorPosition().getZ() >> 4);
|
||||
var start = new StructureStart(holder(UndergroundStructures25.MINER_CABIN).value(), position, 0, new PiecesContainer(List.of(approach, cabin)));
|
||||
add(UndergroundStructures25.MINER_CABIN, start, cabin.getLocatorPosition());
|
||||
} else refuse("cabin:no-accessible-mine-shelter");
|
||||
return;
|
||||
}
|
||||
refuse("mine:no-fitting-native-start");
|
||||
}
|
||||
double caveDistance(StructurePiece piece) {
|
||||
var center = piece.getBoundingBox().getCenter(); double nearest = Double.POSITIVE_INFINITY;
|
||||
for (var chamber : shape.chambers()) if (chamber.kind() == Terrain25Shape.Kind.DRY) {
|
||||
double dx = Math.max(0, Math.abs(center.getX() - chamber.x()) - chamber.rx() * .7);
|
||||
double dz = Math.max(0, Math.abs(center.getZ() - chamber.z()) - chamber.rz() * .7);
|
||||
double dy = piece.getBoundingBox().minY() - chamber.floorY() - 1;
|
||||
nearest = Math.min(nearest, dx * dx + dz * dz + 9 * dy * dy);
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
record HutKey(long chunk, long waterFeature, int waterY) {}
|
||||
record HutCandidate(ExpansionStructures23.Candidate candidate, fr.koka.sanctuary.worldgen.Terrain25Wetlands.Column shore) {}
|
||||
void hut() {
|
||||
int attempts = 0;
|
||||
var candidates = new LinkedHashMap<HutKey, HutCandidate>();
|
||||
// Only real retained water/shore sites contribute candidates. Their altitude follows the existing ground.
|
||||
int radius = root.terrainRadius();
|
||||
for (var region : fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(generator.delegate(root), generator.randomState(root),
|
||||
-radius, -radius, radius, radius)) {
|
||||
var wetlands = Terrain25Runtime.wetlandPlan(generator.delegate(root), generator.randomState(root), region.region());
|
||||
diagnostics.merge("hut:wetland-sites", wetlands.sites().size(), Integer::sum);
|
||||
for (var site : wetlands.sites()) for (int cz = site.minZ() >> 4; cz <= site.maxZ() >> 4; cz++)
|
||||
for (int cx = site.minX() >> 4; cx <= site.maxX() >> 4; cx++) {
|
||||
var chunk = new ChunkPos(cx, cz);
|
||||
var shore = wetlands.column(site, chunk.getMiddleBlockX(), chunk.getMiddleBlockZ());
|
||||
if (shore == null) continue;
|
||||
candidates.putIfAbsent(new HutKey(chunk.pack(), site.waterFeature(), site.waterY()),
|
||||
new HutCandidate(new ExpansionStructures23.Candidate(chunk, mix(root.seed() ^ chunk.pack())), shore));
|
||||
}
|
||||
}
|
||||
diagnostics.put("hut:candidate-chunks", (int) candidates.keySet().stream().map(HutKey::chunk).distinct().count());
|
||||
diagnostics.put("hut:candidate-strata", candidates.size());
|
||||
diagnostics.put("hut:lower-candidate-strata", (int) candidates.values().stream().filter(c -> c.shore().underground()).count());
|
||||
var sampler = generator.randomState(root).createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var resolver = generator.getBiomeSource().createResolver(sampler);
|
||||
// Try naturally admitted lower banks first, then retain the original surface fallback.
|
||||
for (var selected : candidates.values().stream().sorted(Comparator.comparing(c -> !c.shore().underground())).toList()) {
|
||||
var candidate = selected.candidate(); var plannedShore = selected.shore();
|
||||
checkBudget();
|
||||
int x = candidate.chunk().getMiddleBlockX(), z = candidate.chunk().getMiddleBlockZ();
|
||||
if (Math.hypot(x, z) < 64) { refuse("hut:arrival-buffer-candidates"); continue; }
|
||||
refuse("hut:wetland-candidates");
|
||||
if (plannedShore.underground()) refuse("hut:lower-wetland-candidates");
|
||||
boolean water = false;
|
||||
BlockPos shore = null;
|
||||
for (int dz = -12; dz <= 12 && !water; dz += 6) for (int dx = -12; dx <= 12; dx += 6) {
|
||||
int top = plannedShore.underground() ? plannedShore.waterY() : terrain.top(x + dx, z + dz);
|
||||
if (!terrain.at(x + dx, top, z + dz).getFluidState().isEmpty()) { water = true; shore = new BlockPos(x + dx, top, z + dz); break; }
|
||||
}
|
||||
if (!water) { refuse(plannedShore.underground() ? "hut:no-nearby-lower-water" : "hut:no-nearby-surface-water"); continue; }
|
||||
if (attempts++ >= 24) break;
|
||||
refuse("hut:native-preview-candidates");
|
||||
int top = plannedShore.underground() ? plannedShore.floorY() : terrain.top(x, z), nativeTop = top;
|
||||
while (nativeTop > 0 && !terrain.at(x, nativeTop, z).isSolid()) nativeTop--;
|
||||
var biome = generator.structureBiome(resolver.getNoiseBiome(x >> 2, nativeTop >> 2, z >> 2));
|
||||
SanctuaryMod.LOGGER.info("Underground25 hut candidate {} stratum waterY {} lower {} top {} native solid top {} wetland {} native biome {} water {}",
|
||||
candidate.chunk(), plannedShore.waterY(), plannedShore.underground(), top, nativeTop, plannedShore.contains(nativeTop), biome.unwrapKey(), shore);
|
||||
var hut = plannedShore.underground()
|
||||
? ExpansionStructures23.previewAtWetland25(level, generator, root, candidate, plannedShore, deadline)
|
||||
: ExpansionStructures23.previewAt(level, generator, root, "minecraft:swamp_hut", candidate, deadline);
|
||||
if (hut == null || conflict(hut.start().getBoundingBox())) {
|
||||
refuse(hut == null ? "hut:native-preview-refused" : "hut:reserved-box");
|
||||
refuse("hut:shore-refused"); continue;
|
||||
}
|
||||
if (plannedShore.underground()) refuse("hut:lower-native-admitted");
|
||||
add(hut.structure().unwrapKey().orElseThrow(), hut.start(), hut.start().getPieces().getFirst().getLocatorPosition()); return;
|
||||
}
|
||||
refuse("hut:no-native-marsh-shore");
|
||||
}
|
||||
void rooms() {
|
||||
int admitted = 0, tries = 0;
|
||||
// A vanilla room needs a low natural doorway and solid roof. Search beside actual passages,
|
||||
// where this geometry can occur, rather than against the tall open wall of a great cavern.
|
||||
for (var passage : shape.passages()) {
|
||||
if (passage.from().kind() != Terrain25Shape.Kind.DRY || passage.to().kind() != Terrain25Shape.Kind.DRY) continue;
|
||||
double dx = passage.to().x() - passage.from().x(), dz = passage.to().z() - passage.from().z();
|
||||
double length = Math.hypot(dx, dz);
|
||||
if (length < 1) continue;
|
||||
for (int step = 2; step <= 8; step++) for (int side : new int[]{-1, 1}) for (int distance = 2; distance <= 5; distance++) for (int height = -1; height <= 1; height++) {
|
||||
if (admitted >= 3 || tries++ >= 336) { if (admitted == 0) refuse("rooms:no-native-doorway"); return; }
|
||||
checkBudget();
|
||||
double t = step / 10.0;
|
||||
double bend = Math.sin(t * Math.PI * 2 + passage.phase()) * 9 * Math.sin(t * Math.PI);
|
||||
double offset = bend + side * (passage.width() + distance);
|
||||
int x = (int) Math.round(passage.from().x() + t * dx - dz / length * offset);
|
||||
int z = (int) Math.round(passage.from().z() + t * dz + dx / length * offset);
|
||||
int y = (int) Math.floor(passage.from().y() + (passage.to().y() - passage.from().y()) * t
|
||||
+ Math.sin(t * Math.PI * 3 + passage.phase()) * 4 * Math.sin(t * Math.PI)) - 1 + height;
|
||||
long seed = mix(root.seed() ^ ((long) x << 32) ^ z ^ 0xD025L);
|
||||
var random = RandomSource.create(seed); int rx = random.nextInt(2) + 3, rz = random.nextInt(2) + 3;
|
||||
var box = new BoundingBox(x - rx, y - 1, z - rz, x + rx, y + 4, z + rz);
|
||||
if (conflict(box)) continue;
|
||||
if (!monsterRoomFits(terrain, new BlockPos(x, y, z), seed)) continue;
|
||||
var piece = new UndergroundPiece25(UndergroundPiece25.Kind.MONSTER_ROOM, y, seed, new BlockPos(x, y, z), List.of(box));
|
||||
roomFeatures.add(piece); refuse("accepted:monster_room_feature"); admitted++;
|
||||
}
|
||||
}
|
||||
if (admitted == 0) refuse("rooms:no-native-doorway");
|
||||
}
|
||||
}
|
||||
private static boolean monsterRoomFits(Terrain terrain, BlockPos origin, long seed) {
|
||||
var random = RandomSource.create(seed); int rx = random.nextInt(2) + 3, rz = random.nextInt(2) + 3, openings = 0;
|
||||
for (int dz = -rz; dz <= rz; dz++) for (int dx = -rx; dx <= rx; dx++) {
|
||||
int x = origin.getX() + dx, z = origin.getZ() + dz, y = origin.getY();
|
||||
if (!terrain.solid(x, y - 1, z) || !terrain.solid(x, y + 4, z)) return false;
|
||||
for (int dy = -1; dy <= 4; dy++) if (terrain.protectedAt(x, y + dy, z)) return false;
|
||||
if ((Math.abs(dx) == rx || Math.abs(dz) == rz) && terrain.air(x, y, z) && terrain.air(x, y + 1, z)) openings++;
|
||||
}
|
||||
return openings >= 1 && openings <= 5;
|
||||
}
|
||||
/** Candidate search only: no cavity resizing, floor substitution or native graph rewriting. */
|
||||
public static List<ChunkPos> centralVillageChunks(Terrain25Shape.Chamber chamber) {
|
||||
var chunks = new ArrayList<ChunkPos>();
|
||||
int minX = (int) Math.floor((chamber.x() - chamber.rx() * .5) / 16);
|
||||
int maxX = (int) Math.floor((chamber.x() + chamber.rx() * .5) / 16);
|
||||
int minZ = (int) Math.floor((chamber.z() - chamber.rz() * .5) / 16);
|
||||
int maxZ = (int) Math.floor((chamber.z() + chamber.rz() * .5) / 16);
|
||||
for (int z = minZ; z <= maxZ; z++) for (int x = minX; x <= maxX; x++) {
|
||||
var chunk = new ChunkPos(x, z);
|
||||
int cx = chunk.getMiddleBlockX(), cz = chunk.getMiddleBlockZ();
|
||||
if (chamber.horizontal(cx, cz) <= .25 && chamber.cut(cx, chamber.floorY() + 1, cz) > 0) chunks.add(chunk);
|
||||
}
|
||||
return chunks.stream().sorted(Comparator.comparingDouble((ChunkPos c) -> {
|
||||
double dx = c.getMiddleBlockX() - chamber.x(), dz = c.getMiddleBlockZ() - chamber.z();
|
||||
return dx * dx + dz * dz;
|
||||
}).thenComparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).limit(MAX_CENTRAL_VILLAGE_CANDIDATES).toList();
|
||||
}
|
||||
private static final class FloorView extends ExpansionChunkGenerator {
|
||||
final SanctuaryChunkGenerator host; final int floor;
|
||||
FloorView(SanctuaryChunkGenerator host, ServerLevel level, int floor) { super(host.source(), host.initialDiameter(), true); this.host = host; this.floor = floor; this.registries = level.registryAccess(); }
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor height, RandomState state) { return floor; }
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState state) { return host.getBaseColumn(x, z, height, state); }
|
||||
@Override public RandomState randomState(ExpansionIsland island) { return host.randomState(island); }
|
||||
}
|
||||
private static long mix(long value) { value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L; value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL; return value ^ (value >>> 31); }
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** Registry identity for admitted initial-world sites. No random-spread placement or dimension-wide injection. */
|
||||
public final class UndergroundStructure25 extends Structure {
|
||||
public static final MapCodec<UndergroundStructure25> CODEC = simpleCodec(UndergroundStructure25::new);
|
||||
public UndergroundStructure25(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { return Optional.empty(); }
|
||||
@Override public StructureType<?> type() { return UndergroundStructures25.TYPE; }
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package fr.koka.sanctuary.worldgen.underground25;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Initial generation25 only. No injected structure sets, overwritten vanilla pools, or old-world retrofit. */
|
||||
public final class UndergroundStructures25 {
|
||||
public static final ResourceKey<Structure> VILLAGE = key("underground_abandoned_village");
|
||||
public static final ResourceKey<Structure> MINER_CABIN = key("miner_cabin");
|
||||
public static final List<ResourceKey<Structure>> KEYS = List.of(VILLAGE, MINER_CABIN);
|
||||
public static final ResourceKey<LootTable> MINER_LOOT = ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/miner_cabin25"));
|
||||
public static final StructureType<UndergroundStructure25> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("underground_site_v25"), () -> UndergroundStructure25.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("underground_piece_v25"), UndergroundPiece25::new);
|
||||
private static final Map<SanctuaryChunkGenerator, UndergroundPlan25> PLANS = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private UndergroundStructures25() {}
|
||||
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
|
||||
public static void register() {
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, SanctuaryMod.id("monster_room25"), MonsterRoom25Feature.CODEC);
|
||||
}
|
||||
public static UndergroundPlan25 plan(SanctuaryChunkGenerator generator) { return PLANS.getOrDefault(generator, UndergroundPlan25.EMPTY); }
|
||||
public static boolean isPlacing() { return UndergroundPlacement25.isPlacing(); }
|
||||
public static boolean mayWrite(BlockPos pos) { return UndergroundPlacement25.mayWrite(pos); }
|
||||
/** Called once after terrain, core secrets and aerial reservations are available, before requesting new chunks. */
|
||||
public static synchronized void prepare(ServerLevel level) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 25 || !Level.OVERWORLD.equals(level.dimension()) || PLANS.containsKey(generator)) return;
|
||||
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
|
||||
PLANS.put(generator, UndergroundPlan25.EMPTY); return;
|
||||
}
|
||||
long start = System.nanoTime();
|
||||
// During preparation plan() remains EMPTY, so final-column sampling cannot recursively apply this plan.
|
||||
var result = UndergroundPlanner25.plan(level, generator);
|
||||
PLANS.put(generator, result);
|
||||
SanctuaryMod.LOGGER.info("Underground25 seed {}: {} conditional starts, {} columns, {} ms; diagnostics {}",
|
||||
level.getSeed(), result.starts().size(), result.columns(), (System.nanoTime() - start) / 1_000_000, result.diagnostics());
|
||||
}
|
||||
public static void generate(ExpansionChunkGenerator host, RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager manager, ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (!(host instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 25
|
||||
|| !Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures() || !generator.experimentalStructures()) return;
|
||||
for (var planned : plan(generator).starts()) if (planned.start().getChunkPos().equals(chunk.getPos())) {
|
||||
var existing = manager.getStartForStructure(planned.start().getStructure(), chunk);
|
||||
if (existing != null && existing.isValid()) continue;
|
||||
manager.setStartForStructure(planned.start().getStructure(), planned.start(), chunk);
|
||||
}
|
||||
}
|
||||
/** These are vanilla features, not structure starts. FEATURES executes once per new origin chunk. */
|
||||
public static void decorate(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk) {
|
||||
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 25 || !level.getLevel().structureManager().shouldGenerateStructures()
|
||||
|| !generator.experimentalStructures() || !Level.OVERWORLD.equals(level.getLevel().dimension())) return;
|
||||
for (var room : plan(generator).rooms()) if (new net.minecraft.world.level.ChunkPos(room.getLocatorPosition().getX() >> 4, room.getLocatorPosition().getZ() >> 4).equals(chunk.getPos())) {
|
||||
UndergroundPlacement25.placingRoom(room, () -> room.postProcess(level, level.getLevel().structureManager(), generator,
|
||||
net.minecraft.util.RandomSource.create(room.seed()), room.getBoundingBox(), chunk.getPos(), room.getLocatorPosition()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,7 @@
|
||||
"sanctuary.structures.census.status.incompatible": "Incompatible habitat",
|
||||
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s",
|
||||
"sanctuary.world_options.terrain25_hint": "Calcite peaks, natural depths and aerial sites. New oceans contain a monument when structures are enabled.",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.",
|
||||
"sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.",
|
||||
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s",
|
||||
@@ -237,5 +238,19 @@
|
||||
"sanctuary.structures.census.aerial.status.unknown": "unknown",
|
||||
"structure.sanctuary.aerial_balloon": "Hot air balloon",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Mineral islet"
|
||||
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
|
||||
"structure.sanctuary.underground_abandoned_village": "Underground abandoned village",
|
||||
"structure.sanctuary.miner_cabin": "Miner’s cabin",
|
||||
"biome.sanctuary.terrain25_oak_forest": "Oak forest",
|
||||
"biome.sanctuary.terrain25_birch_forest": "Birch forest",
|
||||
"biome.sanctuary.terrain25_clearing": "Flower clearing",
|
||||
"biome.sanctuary.terrain25_dry_woodland": "Dry woodland",
|
||||
"biome.sanctuary.terrain25_rocky_heath": "Rocky heath",
|
||||
"biome.sanctuary.terrain25_dark_grove": "Dark grove",
|
||||
"biome.sanctuary.terrain25_bamboo_grove": "Bamboo grove",
|
||||
"biome.sanctuary.terrain25_sulfur_depths": "Sulfur depths",
|
||||
"biome.sanctuary.terrain25_lush_caves": "Lush caves",
|
||||
"biome.sanctuary.terrain25_dripstone_caves": "Dripstone caves",
|
||||
"biome.sanctuary.terrain25_mushroom_refuge": "Mushroom refuge",
|
||||
"biome.sanctuary.terrain25_swamp": "Sanctuary swamp"
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@
|
||||
"sanctuary.structures.census.status.incompatible": "Milieu incompatible",
|
||||
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s",
|
||||
"sanctuary.world_options.terrain25_hint": "Massifs de calcite, profondeurs naturelles et lieux suspendus. Les nouveaux océans accueillent un monument avec les structures activées.",
|
||||
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.",
|
||||
"sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.",
|
||||
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s",
|
||||
@@ -237,5 +238,19 @@
|
||||
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
|
||||
"structure.sanctuary.aerial_balloon": "Montgolfière",
|
||||
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
|
||||
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral"
|
||||
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
|
||||
"structure.sanctuary.underground_abandoned_village": "Village souterrain abandonné",
|
||||
"structure.sanctuary.miner_cabin": "Cabane du mineur",
|
||||
"biome.sanctuary.terrain25_oak_forest": "Forêt de chênes",
|
||||
"biome.sanctuary.terrain25_birch_forest": "Forêt de bouleaux",
|
||||
"biome.sanctuary.terrain25_clearing": "Clairière fleurie",
|
||||
"biome.sanctuary.terrain25_dry_woodland": "Bois sec",
|
||||
"biome.sanctuary.terrain25_rocky_heath": "Lande rocheuse",
|
||||
"biome.sanctuary.terrain25_dark_grove": "Bosquet sombre",
|
||||
"biome.sanctuary.terrain25_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.terrain25_sulfur_depths": "Profondeurs sulfureuses",
|
||||
"biome.sanctuary.terrain25_lush_caves": "Grottes luxuriantes",
|
||||
"biome.sanctuary.terrain25_dripstone_caves": "Grottes à stalactites",
|
||||
"biome.sanctuary.terrain25_mushroom_refuge": "Refuge à champignons",
|
||||
"biome.sanctuary.terrain25_swamp": "Marais de Sanctuary"
|
||||
}
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 4,
|
||||
"max": 7
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"weight": 7,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 8,
|
||||
"max": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:iron_ingot",
|
||||
"weight": 5,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 3,
|
||||
"max": 8
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:gold_ingot",
|
||||
"weight": 3,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 2,
|
||||
"max": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:rail",
|
||||
"weight": 5,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 8,
|
||||
"max": 20
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:bread",
|
||||
"weight": 4,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 3,
|
||||
"max": 7
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond",
|
||||
"weight": 3,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 1,
|
||||
"max": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:golden_apple",
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond_pickaxe",
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:emerald",
|
||||
"weight": 3,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"min": 2,
|
||||
"max": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+13
-1
@@ -50,6 +50,18 @@
|
||||
"sanctuary:woodland_dry_woodland",
|
||||
"sanctuary:woodland_oak_forest",
|
||||
"sanctuary:woodland_rocky_heath",
|
||||
"sanctuary:woodland_sulfur_depths"
|
||||
"sanctuary:woodland_sulfur_depths",
|
||||
"sanctuary:terrain25_oak_forest",
|
||||
"sanctuary:terrain25_birch_forest",
|
||||
"sanctuary:terrain25_clearing",
|
||||
"sanctuary:terrain25_dry_woodland",
|
||||
"sanctuary:terrain25_rocky_heath",
|
||||
"sanctuary:terrain25_dark_grove",
|
||||
"sanctuary:terrain25_bamboo_grove",
|
||||
"sanctuary:terrain25_sulfur_depths",
|
||||
"sanctuary:terrain25_lush_caves",
|
||||
"sanctuary:terrain25_dripstone_caves",
|
||||
"sanctuary:terrain25_mushroom_refuge",
|
||||
"sanctuary:terrain25_swamp"
|
||||
]
|
||||
}
|
||||
|
||||
+13
-1
@@ -50,6 +50,18 @@
|
||||
"sanctuary:woodland_dry_woodland",
|
||||
"sanctuary:woodland_oak_forest",
|
||||
"sanctuary:woodland_rocky_heath",
|
||||
"sanctuary:woodland_sulfur_depths"
|
||||
"sanctuary:woodland_sulfur_depths",
|
||||
"sanctuary:terrain25_oak_forest",
|
||||
"sanctuary:terrain25_birch_forest",
|
||||
"sanctuary:terrain25_clearing",
|
||||
"sanctuary:terrain25_dry_woodland",
|
||||
"sanctuary:terrain25_rocky_heath",
|
||||
"sanctuary:terrain25_dark_grove",
|
||||
"sanctuary:terrain25_bamboo_grove",
|
||||
"sanctuary:terrain25_sulfur_depths",
|
||||
"sanctuary:terrain25_lush_caves",
|
||||
"sanctuary:terrain25_dripstone_caves",
|
||||
"sanctuary:terrain25_mushroom_refuge",
|
||||
"sanctuary:terrain25_swamp"
|
||||
]
|
||||
}
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.sparse_jungle"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:wolf",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.95,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:terrain25_monster_room",
|
||||
"sanctuary:terrain25_monster_room_deep"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"sanctuary:terrain25_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane",
|
||||
"sanctuary:terrain25_refuge"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.8
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.forest"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": 4,
|
||||
"weight": 12
|
||||
},
|
||||
{
|
||||
"type": "minecraft:pig",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:chicken",
|
||||
"count": 4,
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:cow",
|
||||
"count": 4,
|
||||
"weight": 8
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.7,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:terrain25_monster_room",
|
||||
"sanctuary:terrain25_monster_room_deep"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:forest_flowers",
|
||||
"minecraft:wildflowers_birch_forest",
|
||||
"minecraft:trees_birch",
|
||||
"minecraft:patch_bush",
|
||||
"minecraft:flower_default",
|
||||
"minecraft:patch_grass_forest",
|
||||
"minecraft:brown_mushroom_normal",
|
||||
"minecraft:red_mushroom_normal",
|
||||
"minecraft:patch_pumpkin",
|
||||
"minecraft:patch_sugar_cane",
|
||||
"minecraft:patch_firefly_bush_near_water",
|
||||
"sanctuary:terrain25_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane",
|
||||
"sanctuary:terrain25_refuge"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"attributes": {
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.overworld.meadow"
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/natural_mob_spawns": {
|
||||
"argument": {
|
||||
"spawn_costs": {},
|
||||
"spawns_by_category": {
|
||||
"ambient": [
|
||||
{
|
||||
"type": "minecraft:bat",
|
||||
"count": 8,
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"creature": [
|
||||
{
|
||||
"type": "minecraft:donkey",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 2,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 1
|
||||
},
|
||||
{
|
||||
"type": "minecraft:rabbit",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"type": "minecraft:sheep",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 2
|
||||
},
|
||||
"weight": 2
|
||||
}
|
||||
],
|
||||
"monster": [
|
||||
{
|
||||
"type": "minecraft:spider",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"count": 4,
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"type": "minecraft:zombie_villager",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
},
|
||||
{
|
||||
"type": "minecraft:skeleton",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:creeper",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:slime",
|
||||
"count": 4,
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"type": "minecraft:enderman",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 4,
|
||||
"min_inclusive": 1
|
||||
},
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"type": "minecraft:witch",
|
||||
"count": 1,
|
||||
"weight": 5
|
||||
}
|
||||
],
|
||||
"underground_water_creature": [
|
||||
{
|
||||
"type": "minecraft:glow_squid",
|
||||
"count": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": 6,
|
||||
"min_inclusive": 4
|
||||
},
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"modifier": "overlay"
|
||||
},
|
||||
"minecraft:visual/sky_color": "#79a6ff"
|
||||
},
|
||||
"carvers": [],
|
||||
"downfall": 0.75,
|
||||
"effects": {
|
||||
"water_color": "#3f76e4",
|
||||
"grass_color": "#88b96b",
|
||||
"foliage_color": "#79aa61",
|
||||
"dry_foliage_color": "#b9a766"
|
||||
},
|
||||
"features": [
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"sanctuary:terrain25_monster_room",
|
||||
"sanctuary:terrain25_monster_room_deep"
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
"minecraft:glow_lichen",
|
||||
"minecraft:patch_tall_grass_2",
|
||||
"minecraft:patch_grass_meadow",
|
||||
"minecraft:flower_meadow",
|
||||
"minecraft:trees_meadow",
|
||||
"minecraft:wildflowers_meadow",
|
||||
"minecraft:flower_flower_forest",
|
||||
"sanctuary:terrain25_groves",
|
||||
"sanctuary:population_decorations",
|
||||
"sanctuary:population_shore_sugar_cane",
|
||||
"sanctuary:terrain25_refuge"
|
||||
],
|
||||
[
|
||||
"minecraft:freeze_top_layer"
|
||||
]
|
||||
],
|
||||
"has_precipitation": true,
|
||||
"temperature": 0.7
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user