feat: natural Sanctuary secrets and guaranteed expansions for alpha23.1
This commit is contained in:
@@ -20,6 +20,20 @@ publiée** : le placement des structures redevient sélectif. Les palettes,
|
|||||||
intérieurs et filons de l’alpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
|
intérieurs et filons de l’alpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
|
||||||
et [la distribution](docs/packwiz.md).
|
et [la distribution](docs/packwiz.md).
|
||||||
|
|
||||||
|
## Alpha.23.1 — livraison locale vérifiée
|
||||||
|
|
||||||
|
Le [chantier de génération](docs/generation-alpha23.1.md) réunit le Blocodex, une
|
||||||
|
île initiale naturelle avec quelques secrets, le sanctuaire minéral, les océans
|
||||||
|
et le catalogue des structures vanilla. Chaque nouvelle expansion acceptée, avec
|
||||||
|
les structures activées, prévoit au moins une structure adaptée avant sa réservation.
|
||||||
|
Les familles absentes des régions entièrement vérifiées sont favorisées.
|
||||||
|
|
||||||
|
`check build`, `assemblePack`, les essais natifs et le MRpack passent sur Mac.
|
||||||
|
Voir les [vérifications et limites](docs/testing-alpha23.1.md). Créer un nouveau
|
||||||
|
monde Sanctuary pour `island_v23` ; les anciennes sauvegardes gardent leur génération.
|
||||||
|
La publication reste bloquée par les identifiants Git indisponibles. Le canal
|
||||||
|
packwiz reste à l’alpha.22 et l’instance Prism n’a pas été modifiée par cette livraison.
|
||||||
|
|
||||||
## Alpha.23 — Blocodex et socle commun de données, livraison locale
|
## Alpha.23 — Blocodex et socle commun de données, livraison locale
|
||||||
|
|
||||||
Le Blocodex fait partie de Sanctuary et s’ouvre avec **B**, touche modifiable
|
Le Blocodex fait partie de Sanctuary et s’ouvre avec **B**, touche modifiable
|
||||||
|
|||||||
@@ -925,3 +925,19 @@ Ces thèmes servent à retrouver la vision, pas à demander leur implémentation
|
|||||||
## Définition pratique d'un ticket terminé
|
## Définition pratique d'un ticket terminé
|
||||||
|
|
||||||
Un ticket contient un résultat observable, un périmètre limité et des critères d'acceptation vérifiables. Sa conclusion indique le comportement livré, la version testée, les vérifications réellement exécutées et les limitations qui subsistent. Les nouveaux besoins découverts deviennent de nouveaux tickets plutôt que des ajouts implicites à tous les systèmes.
|
Un ticket contient un résultat observable, un périmètre limité et des critères d'acceptation vérifiables. Sa conclusion indique le comportement livré, la version testée, les vérifications réellement exécutées et les limitations qui subsistent. Les nouveaux besoins découverts deviennent de nouveaux tickets plutôt que des ajouts implicites à tous les systèmes.
|
||||||
|
|
||||||
|
### WG-27 — Île naturelle, sanctuaire minéral et structures natives — alpha.23.1
|
||||||
|
|
||||||
|
**Implémenté et vérifié localement.** Branche `codex/natural-secrets-alpha23`,
|
||||||
|
depuis le Blocodex alpha.23. MRpack alpha.23.1 produit ; publication et
|
||||||
|
synchronisation Prism en attente de l’accès Git.
|
||||||
|
|
||||||
|
Le [contrat](generation-alpha23.1.md) conserve les secrets, le hall et les voies,
|
||||||
|
retire les bâtiments Sanctuary ordinaires de l’île initiale, introduit le
|
||||||
|
sanctuaire minéral et les océans d’expansion. Le catalogue vanilla est conservé
|
||||||
|
et étendu, avec registre persistant et priorités figées. Chaque expansion doit
|
||||||
|
prévoir au moins une structure adaptée avant réservation, sans placement forcé.
|
||||||
|
Deep Dark et cités anciennes attendent les Cavernes.
|
||||||
|
|
||||||
|
Les [preuves de validation](testing-alpha23.1.md) séparent intentions, tests
|
||||||
|
exécutés et état de la distribution. Aucun boss, portail actif ou raid livré ici.
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Alpha.23.1 — île naturelle, secrets et continents
|
||||||
|
|
||||||
|
Contrat du ticket **WG-27**, branche `codex/natural-secrets-alpha23`.
|
||||||
|
Cette livraison réunit le Blocodex alpha.23 et la génération `sanctuary:island_v23`.
|
||||||
|
Les résultats des essais sont consignés dans [le rapport](testing-alpha23.1.md).
|
||||||
|
|
||||||
|
## Sanctuary Island
|
||||||
|
|
||||||
|
Le point de départ reste naturel. Les quartiers Lost City et les infrastructures
|
||||||
|
ordinaires Sanctuary ne sont plus demandés sur l’île initiale. Leur code et les
|
||||||
|
anciennes générations sont conservés ; les expansions Lost City restent possibles.
|
||||||
|
Les structures vanilla ne sont pas supprimées : leur admission dépend du biome,
|
||||||
|
du relief, des appuis et des volumes déjà occupés.
|
||||||
|
|
||||||
|
La sélection des lieux Sanctuary conserve l’observatoire, un atelier caché,
|
||||||
|
la grande traversée et la salle d’expansion indépendante. La piscine attenante
|
||||||
|
est retirée ; les égouts et leurs ouvertures restent possibles. Les anciennes voies
|
||||||
|
ferrées peuvent exister sans gare ni bâtiment au bout : leur remise en service
|
||||||
|
reste une construction des joueurs.
|
||||||
|
|
||||||
|
Le **sanctuaire minéral** est une grande cavité géologique explorable, dominante
|
||||||
|
pierre et cobblestone, avec voûtes, piliers, corniches et filons minables. Il peut
|
||||||
|
affleurer sur les flancs ou sous l’île avec des attaches rocheuses. L’enveloppe
|
||||||
|
maximale est 79 × 73 × 27 ; ce volume ne désigne pas une façade rectangulaire.
|
||||||
|
Il ne comporte ni briques, mobilier, machines, ni éclairage manufacturé.
|
||||||
|
Le mot laboratoire reste réservé à la future structure des Mooblooms.
|
||||||
|
|
||||||
|
Le gardien et le portail des Cavernes ne sont pas activés ici. L’intention de
|
||||||
|
progression demeure : vaincre le gardien stabilisera l’accès aux Cavernes pour
|
||||||
|
tout le serveur, puis chacun pourra construire son portail. Le Deep Dark et les
|
||||||
|
cités anciennes appartiendront à ces Cavernes. Le lieu des raids appartient aux
|
||||||
|
expansions et n’est pas construit dans cette livraison.
|
||||||
|
|
||||||
|
## Structures natives et garantie d’expansion
|
||||||
|
|
||||||
|
Le catalogue s’appuie sur les 52 identifiants de structures présents dans le JAR
|
||||||
|
Minecraft 26.3-pre-2 : 47 de l’Overworld, dont la cité ancienne réservée aux Cavernes,
|
||||||
|
et cinq propres au Nether ou à l’End. Les petits donjons `monster_room` sont des
|
||||||
|
features et font l’objet d’un suivi séparé. Les modèles, identifiants, coffres,
|
||||||
|
spawners et comportements natifs sont conservés.
|
||||||
|
|
||||||
|
Les placements distinguent surface, sous-sol et milieu aquatique. Les hauteurs
|
||||||
|
souterraines vanilla ne sont pas recopiées sous Y=0 : elles sont adaptées à la
|
||||||
|
roche de l’île. Les monuments conservent aussi leur hauteur lors du rechargement.
|
||||||
|
L’adaptation ne remplace pas les définitions `minecraft:*` du registre.
|
||||||
|
Les ruines océaniques et les trésors enterrés conservent leur ajustement fin
|
||||||
|
vanilla au fond réel pendant la pose des blocs : leur hauteur peut encore se
|
||||||
|
préciser après l’acceptation du départ.
|
||||||
|
|
||||||
|
**Correction approuvée après le plan initial : chaque nouvelle expansion 23 avec
|
||||||
|
les structures activées doit disposer d’au moins une structure adaptée avant sa
|
||||||
|
réservation.** Les familles absentes restent préférées, sans rendre obligatoire
|
||||||
|
une famille incompatible. Une recherche infructueuse ne publie pas une île vide
|
||||||
|
de structures et ne contourne pas les exigences d’appui. Aucune structure n’est
|
||||||
|
ajoutée rétroactivement aux îles déjà enregistrées.
|
||||||
|
|
||||||
|
Les yeux de l’Ender, cartes d’exploration et `/locate` recherchent les vrais
|
||||||
|
départs dans les îles ouvertes. Une recherche n’active jamais d’expansion.
|
||||||
|
Le compteur natif de références cartographiques n’indique pas une visite réelle.
|
||||||
|
|
||||||
|
## Océans
|
||||||
|
|
||||||
|
Le relief `ocean` est réservé aux expansions 23 de 512 ou 1024 blocs. Les grandes
|
||||||
|
expansions rapides sans relief explicite peuvent le choisir dans un tirage
|
||||||
|
déterministe d’environ un tiers. Le climat et la taille restent ceux du descripteur.
|
||||||
|
|
||||||
|
Le champ océanique commun au terrain, à l’eau et aux biomes produit une mer
|
||||||
|
intérieure irrégulière, des hauts-fonds, des côtes et une zone profonde, en
|
||||||
|
conservant une majorité de terres. Cible : 30–45 % de surface aquatique nominale,
|
||||||
|
profondeurs maximales 32–56 blocs. Une coque rocheuse continue retient l’eau ;
|
||||||
|
le reste du vide conserve l’air. Le niveau d’eau est local à l’expansion.
|
||||||
|
Ce niveau accompagne aussi les décorations natives, notamment les icebergs ;
|
||||||
|
il est rétabli après chaque passe pour ne pas affecter les autres îles.
|
||||||
|
|
||||||
|
Les températures utilisent les biomes natifs océan, océan froid, gelé, tiède et
|
||||||
|
chaud, avec les variantes profondes disponibles. Il n’existe pas de
|
||||||
|
`deep_warm_ocean` natif. Les monuments exigent un océan profond réellement adapté.
|
||||||
|
Aucun océan ni Deep Dark n’est ajouté à Sanctuary Island.
|
||||||
|
|
||||||
|
## Mémoire serveur et sauvegardes
|
||||||
|
|
||||||
|
Le registre des structures est persistant et distinct du census des seuls chunks
|
||||||
|
chargés. Il distingue départ accepté, bâtiment partiellement ou complètement
|
||||||
|
généré, région encore inconnue et absence confirmée dans une région vérifiée.
|
||||||
|
Une démolition ne supprime pas le fait historique de génération. Une erreur de
|
||||||
|
lecture ne transforme pas le registre en liste vide.
|
||||||
|
|
||||||
|
`/sanctuary census structures` expose les relevés aux opérateurs ; l’option
|
||||||
|
`island <id>` précise la région et `dimension <id>` distingue les dimensions natives.
|
||||||
|
L’île propriétaire se déduit des descripteurs immuables du journal d’expansion.
|
||||||
|
Le suivi automatique ne charge pas la carte et
|
||||||
|
ne révèle pas les secrets dans le Blocodex personnel.
|
||||||
|
|
||||||
|
Le champ optionnel plat `structure_priorities` d’une nouvelle expansion 23
|
||||||
|
contient les familles bonifiées. Il est capturé une fois avant l’inspection,
|
||||||
|
figé dans le journal et lié au certificat de réutilisation du vide. Bonus initial
|
||||||
|
de sélection : × 4. Une reprise réutilise ce profil même si le registre a évolué.
|
||||||
|
Les générations historiques restent neutres, sans champ ajouté à leurs écritures.
|
||||||
|
|
||||||
|
Les mondes existants gardent leur codec et leur terrain. Les nouvelles règles
|
||||||
|
nécessitent un nouveau monde ; aucune conversion ou régénération n’est effectuée.
|
||||||
|
Petit 512, Moyen 724 par défaut et Grand 1024 restent dans Personnaliser. L’option
|
||||||
|
expérimentale commande les secrets optionnels et les rails ; le hall et la
|
||||||
|
traversée restent dans le socle. Les structures vanilla en sont indépendantes.
|
||||||
|
La désactivation générale des structures reste prioritaire.
|
||||||
@@ -13,6 +13,28 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
|||||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||||
de l'ancien pack 26.2.
|
de l'ancien pack 26.2.
|
||||||
|
|
||||||
|
## Alpha.23.1 locale vérifiée — 11 septembre 2026
|
||||||
|
|
||||||
|
Le JAR et `build/Sanctuary-0.1.0-alpha.23.1.mrpack` sont construits et vérifiés
|
||||||
|
après `check build` et `assemblePack`. Les versions Minecraft 26.3-pre-2,
|
||||||
|
Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 sont conservées.
|
||||||
|
Voir [les validations](testing-alpha23.1.md) et
|
||||||
|
[le contrat de génération](generation-alpha23.1.md).
|
||||||
|
|
||||||
|
SHA-256 du JAR :
|
||||||
|
|
||||||
|
```text
|
||||||
|
906fd2e12be4233676a1a561556508eee7412183698c2cac704dc1191324f136
|
||||||
|
```
|
||||||
|
|
||||||
|
**Publication en attente :** le gestionnaire d’identifiants Git ne fournit pas
|
||||||
|
l’authentification nécessaire. Le canal public reste à l’alpha.22 ; aucune
|
||||||
|
synchronisation de l’instance Prism n’a été effectuée pour l’alpha.23.1.
|
||||||
|
Le MRpack local peut être conservé pour les essais. Le ZIP d’amorçage préparé
|
||||||
|
suit encore le canal public alpha.22 et ne doit pas être présenté comme une
|
||||||
|
installation publiée de l’alpha.23.1. Les étapes de publication puis de
|
||||||
|
synchronisation ci-dessous restent à exécuter après rétablissement de l’accès.
|
||||||
|
|
||||||
## Alpha.22 publiée — 10 septembre 2026
|
## Alpha.22 publiée — 10 septembre 2026
|
||||||
|
|
||||||
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
|
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# Validation alpha.23.1
|
||||||
|
|
||||||
|
Cible : Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API
|
||||||
|
0.160.0+26.3, Java 25. Branche `codex/natural-secrets-alpha23`, ticket WG-27.
|
||||||
|
|
||||||
|
**Livraison locale vérifiée le 11 septembre 2026.** Les neuf cas de la matrice
|
||||||
|
initiale, les structures natives et leurs réouvertures, les océans échantillonnés
|
||||||
|
et le cycle réel d’expansion passent. `check build`, `assemblePack` et le MRpack
|
||||||
|
sont vérifiés. **Publication et synchronisation Prism restent bloquées par
|
||||||
|
l’authentification Git indisponible.** Le [contrat de génération](generation-alpha23.1.md)
|
||||||
|
décrit le périmètre ; les limites de chaque preuve sont conservées ci-dessous.
|
||||||
|
|
||||||
|
## Environnement et preuves
|
||||||
|
|
||||||
|
Les essais utilisent un serveur Fabric GameTest sans interface graphique sur
|
||||||
|
Mac, dans des mondes de développement ignorés. Aucun monde personnel n’a été
|
||||||
|
ouvert ou modifié. **Windows n’a pas été testé ; aucune validation visuelle
|
||||||
|
client, avec ou sans shaders, n’a été effectuée.**
|
||||||
|
|
||||||
|
Les preuves locales sont dans `build/alpha231-evidence/matrix.json` et les
|
||||||
|
rapports `secrets-<graine>-<diamètre>.json` du même dossier. Pour le Moyen,
|
||||||
|
graine 0, les rapports finaux `final-medium.json` et `final-medium-reload.json`
|
||||||
|
remplacent le résultat intermédiaire de `secrets-0-724.json` ; leurs durées
|
||||||
|
figurent dans `postchecks.json`. Les réouvertures possèdent un rapport
|
||||||
|
`*-reload.json`. Ces fichiers de développement ne sont
|
||||||
|
pas des artefacts publics versionnés. Les journaux correspondants sont indiqués
|
||||||
|
dans la matrice ; les assertions sont dans
|
||||||
|
[Secrets23WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Secrets23WorldGameTests.java).
|
||||||
|
|
||||||
|
Les premières exécutions précèdent la dernière optimisation du préfiltre
|
||||||
|
minéral. La distinction est conservée ci-dessous, avec la reprise du Moyen,
|
||||||
|
graine 0, et la réouverture du Petit, graine 42, sur le préfiltre final.
|
||||||
|
|
||||||
|
## Créations réellement vérifiées en FULL
|
||||||
|
|
||||||
|
Les neuf cas ci-dessous ont `passed: true` et un code de sortie nul. Dans ces
|
||||||
|
témoins, aucun quartier Lost City n’est demandé sur l’île initiale ; un
|
||||||
|
observatoire et un hall indépendant sont toujours présents. Le réseau d’atelier
|
||||||
|
caché apparaît dans huit cas sur neuf : il n’est pas retenu dans le Petit,
|
||||||
|
graine 0. Lorsqu’il est présent, il ne contient que son accès, un corridor et
|
||||||
|
l’atelier. Le hall n’inclut aucune piscine. Les structures expérimentales sont
|
||||||
|
activées.
|
||||||
|
|
||||||
|
| Graine | Taille nominale | Chunks FULL contrôlés | Modules de traversée | Liaisons patrimoniales | Rails natifs vérifiés | Sanctuaire minéral | État |
|
||||||
|
| --- | --- | ---: | ---: | ---: | ---: | --- | --- |
|
||||||
|
| 0 | Petit 512 | 88 | 19 | 2 | 85 | Présent | Réussi avec le préfiltre final ; atelier caché non retenu |
|
||||||
|
| 0 | Moyen 724 | 85 | 23 | 2 | 68 | Présent | Création et réouverture finales réussies (`final-medium*.json`) |
|
||||||
|
| 0 | Grand 1 024 | 105 | 35 | 2 | 73 | Présent | Réussi avec le préfiltre final |
|
||||||
|
| 42 | Petit 512 | 88 | 17 | 1 | 23 | Présent | Création réussie ; réouverture avec le préfiltre final réussie |
|
||||||
|
| 42 | Moyen 724 | 82 | 23 | 3 | 135 | Présent | Réussi avec le préfiltre final |
|
||||||
|
| 42 | Grand 1 024 | 125 | 31 | 1 | 43 | Présent | Réussi avec le préfiltre final |
|
||||||
|
| 2026 | Petit 512 | 85 | 17 | 2 | 36 | Présent | Réussi avec le préfiltre final |
|
||||||
|
| 2026 | Moyen 724 | 79 | 23 | 1 | 54 | Présent | Réussi avec le préfiltre final |
|
||||||
|
| 2026 | Grand 1 024 | 107 | 31 | 2 | 58 | Présent | Réussi avec le préfiltre final |
|
||||||
|
|
||||||
|
Le nombre de chunks contrôlés correspond aux emprises des structures retenues,
|
||||||
|
pas à toute l’île. Ces neuf témoins ne garantissent pas le même résultat sur
|
||||||
|
toutes les graines.
|
||||||
|
Le nombre de liaisons ne signifie pas que tous les lieux sont reliés : les
|
||||||
|
tronçons ferroviaires locaux peuvent subsister quand un raccord lointain est
|
||||||
|
refusé par le terrain.
|
||||||
|
|
||||||
|
Les durées du runner consignées pour les neuf créations retenues vont de
|
||||||
|
68,38 à 148,52 secondes. Elles incluent le lancement de la JVM, la
|
||||||
|
préparation du monde, la génération FULL des emprises et les assertions.
|
||||||
|
|
||||||
|
| Graine | Petit 512 | Moyen 724 | Grand 1 024 |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| 0 | 78,86 s | 96,19 s | 148,52 s |
|
||||||
|
| 42 | 68,38 s | 79,60 s | 113,23 s |
|
||||||
|
| 2026 | 70,20 s | 89,04 s | 133,05 s |
|
||||||
|
|
||||||
|
Le Moyen, graine 0, utilise ici la durée de sa création finale dans
|
||||||
|
`postchecks.json`, et non les 99,07 secondes de son ancien essai. Sa réouverture
|
||||||
|
finale prend 57,24 secondes.
|
||||||
|
|
||||||
|
**Ce ne sont ni des temps de rendu client, ni un benchmark Windows, ni une
|
||||||
|
mesure du gain de performance par rapport à l’alpha.22.**
|
||||||
|
|
||||||
|
## Ce que le GameTest contrôle
|
||||||
|
|
||||||
|
- Chaque emplacement accepté possède un vrai départ de structure natif. Les
|
||||||
|
pièces sauvegardées retrouvent les mêmes données après sérialisation puis
|
||||||
|
lecture ; leurs emprises sont chargées jusqu’au statut FULL.
|
||||||
|
- La traversée est parcourue par un contrôle des collisions natives pour un
|
||||||
|
volume de joueur de 0,6 × 1,8 bloc. Les montées souterraines exigent un escalier
|
||||||
|
orienté ; les abords naturels des portiques peuvent comporter un saut normal
|
||||||
|
d’un bloc. La trappe, les échelles et les ouvertures du hall sont contrôlées.
|
||||||
|
- Toutes les cellules marquées comme portant un rail possèdent réellement un
|
||||||
|
bloc de rail après décoration, avec un ballast solide dessous. Les lacunes
|
||||||
|
prévues restent réparables ; aucun trajet réel en wagon n’a été testé.
|
||||||
|
- Le centre du sanctuaire minéral est contrôlé par échantillonnage dans les
|
||||||
|
chunks FULL : sol solide et deux blocs d’air au-dessus. Le test pur vérifie
|
||||||
|
aussi l’intégralité du centre de 39 × 39 et les quatre orientations.
|
||||||
|
- Les voies de surface préservent le disque naturel d’arrivée de 48 blocs de
|
||||||
|
rayon. Les volumes retenus ne nécessitent aucune ville de surface.
|
||||||
|
|
||||||
|
Ces contrôles ne constituent pas une appréciation esthétique des ruines ou des
|
||||||
|
cavités. La présence de géométrie et la circulation vérifiée ne valident pas
|
||||||
|
encore l’équilibrage des ressources, l’expérience multijoueur ou les effets
|
||||||
|
visuels de l’eau et de la lumière.
|
||||||
|
|
||||||
|
## Repères de visite — graine 42
|
||||||
|
|
||||||
|
Ces coordonnées proviennent des géométries des rapports. Elles s’appliquent à
|
||||||
|
un **nouveau monde de génération 23**, avec les structures et les secrets
|
||||||
|
expérimentaux activés, dans la taille correspondante. Ce sont des repères de
|
||||||
|
développement ; aucune téléportation dans un monde personnel n’a été effectuée.
|
||||||
|
|
||||||
|
| Lieu ou accès, coordonnées X Y Z | Petit 512 | Moyen 724 | Grand 1 024 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Entrée de l’observatoire | `54 243 -40` | `186 251 -110` | `204 250 -121` |
|
||||||
|
| Accès de l’atelier caché | `-45 244 -131` | `-116 250 -157` | `-174 251 -128` |
|
||||||
|
| Trappe du hall indépendant | `32 168 -72` | `-97 223 -53` | `46 256 146` |
|
||||||
|
| Premier portique parcouru | `-68 250 -93` | `-170 256 82` | `-272 249 -8` |
|
||||||
|
| Second portique parcouru | `68 250 43` | `170 252 82` | `204 250 -8` |
|
||||||
|
| Point de localisation intérieur du sanctuaire minéral | `-144 165 -61` | `-121 208 -23` | `-103 144 194` |
|
||||||
|
| Coin minimal de son enveloppe | `-183 161 -97` | `-160 204 -59` | `-142 140 158` |
|
||||||
|
|
||||||
|
L’enveloppe minérale conserve 79 × 73 × 27 blocs — largeur, profondeur, hauteur —
|
||||||
|
et un centre dégagé de 39 × 39. Une ouverture sur un flanc exposé peut donner
|
||||||
|
sur le vide ; les essais ne garantissent pas un chemin extérieur déjà construit
|
||||||
|
jusqu’à elle. Le point de localisation reste dans la salle.
|
||||||
|
|
||||||
|
## Réouverture et limites de l’admission minérale
|
||||||
|
|
||||||
|
Le Petit, graine 42, passe sa réouverture dans une JVM distincte : processus
|
||||||
|
7581 pour la création et 7664 pour la lecture. Les paramètres du générateur,
|
||||||
|
les plans, les départs natifs et la modification témoin de l’observatoire sont
|
||||||
|
conservés. Le sanctuaire reste présent, avec 23 rails natifs dans le monde.
|
||||||
|
La création précédait le préfiltre final ; sa réouverture, avec ce préfiltre,
|
||||||
|
conserve la même géométrie. Ce cas ne valide pas toutes les sauvegardes.
|
||||||
|
|
||||||
|
Le Moyen, graine 0, passe sa création et sa réouverture finales dans deux JVM
|
||||||
|
distinctes, processus 8650 puis 8777 (`final-medium.json` et
|
||||||
|
`final-medium-reload.json`). Le sanctuaire est présent, avec 85 chunks FULL
|
||||||
|
contrôlés et 68 rails natifs. Les paramètres du générateur, les géométries,
|
||||||
|
les départs natifs et la modification témoin sauvegardée sont identiques entre
|
||||||
|
les deux rapports. Le point de localisation intérieur du sanctuaire est
|
||||||
|
`-93 143 81`, dans une enveloppe dont le coin minimal est `-132 139 45`.
|
||||||
|
Ce résultat final remplace le refus minéral de la recherche intermédiaire.
|
||||||
|
|
||||||
|
La recherche du sanctuaire reste bornée : 48 positions, quatre orientations,
|
||||||
|
trois profondeurs, au plus 10 000 colonnes et 1 200 000 sondages. Le préfiltre
|
||||||
|
examine une grille avant la certification détaillée. Celle-ci conserve au moins
|
||||||
|
60 % de couverture et d’appuis directs sur deux blocs de roche native ; chaque
|
||||||
|
colonne sans appui direct doit être à dix blocs au plus d’un appui. Le volume
|
||||||
|
protégé est vérifié intégralement. Le rendu ne pose aucun pilier hors de son
|
||||||
|
enveloppe pour sauver un mauvais emplacement.
|
||||||
|
|
||||||
|
Les diagnostics distinguent ouverture obstruée, couverture ou roche
|
||||||
|
insuffisantes, appuis trop éloignés, volumes protégés et budget épuisé. Un refus
|
||||||
|
ne prouve pas qu’aucun emplacement imaginable n’existe ailleurs dans l’île.
|
||||||
|
Les témoins obtenus ne garantissent donc pas la présence du sanctuaire pour
|
||||||
|
toutes les graines. Les règles des autres structures n’ont pas été assouplies
|
||||||
|
pour faire accepter ce lieu.
|
||||||
|
|
||||||
|
## Autres vérifications
|
||||||
|
|
||||||
|
### Réalisées
|
||||||
|
|
||||||
|
Le smoke ciblé `secrets23Smoke` a passé ses témoins de géométrie, sauvegarde et
|
||||||
|
refus ; `structureCensusSmoke` possède également une réussite dans le journal
|
||||||
|
local `build/alpha23.1-corrected-smokes.log`.
|
||||||
|
|
||||||
|
La suite générale réussit ses **24 GameTests obligatoires** dans
|
||||||
|
`build/alpha23.1-general.log` : 51,84 secondes pour l’exécution des tests et
|
||||||
|
117,75 secondes pour le runner complet. La dernière entrée `general` de
|
||||||
|
`postchecks.json` possède un code de sortie nul. Elle remplace le premier
|
||||||
|
essai interrompu par l’ancienne assertion du nombre de biomes, depuis
|
||||||
|
actualisée. Les suites spécialisées et les commandes finales sont attestées
|
||||||
|
séparément dans les sections suivantes.
|
||||||
|
|
||||||
|
Les deux modes OFF passent dans les mondes de développement de graine 0,
|
||||||
|
taille Moyen 724. Les reçus et les durées se trouvent dans
|
||||||
|
`build/alpha231-evidence/` et `postchecks.json` :
|
||||||
|
|
||||||
|
| Option | Preuve | Résultat contrôlé | Durée du runner |
|
||||||
|
| --- | --- | --- | ---: |
|
||||||
|
| `sanctuaryExperimentalStructures=false` | `optional-off.json` | Aucun observatoire, atelier, sanctuaire minéral ni rail patrimonial ; les 23 modules de traversée et le hall indépendant restent présents. 43 chunks FULL contrôlés, parcours de la traversée et accès du hall réussis. | 69,04 s |
|
||||||
|
| `sanctuaryGenerateStructures=false` | `global-off.json` | Aucun départ natif valide dans les trois chunks contrôlés ; 293 376 positions vérifiées sans les rails et blocs manufacturés ciblés par le test. | 30,50 s |
|
||||||
|
|
||||||
|
Ces essais attestent les comportements observés dans leurs emprises ; le
|
||||||
|
contrôle global OFF n’est pas un inventaire de tous les chunks de l’île.
|
||||||
|
|
||||||
|
La création puis la réouverture du générateur historique
|
||||||
|
`sanctuary:island_v22` passent également pour la graine 0, taille Moyen 724,
|
||||||
|
avec les structures expérimentales activées. Les rapports `legacy22.json`
|
||||||
|
et `legacy22-reload.json` proviennent de JVM distinctes, processus 9088 puis
|
||||||
|
9208, en 130,05 et 86,11 secondes. Ils contrôlent 115 chunks FULL et conservent
|
||||||
|
14 sites patrimoniaux, sept liaisons, 23 modules de traversée, un hall indépendant
|
||||||
|
et la modification témoin sauvegardée.
|
||||||
|
|
||||||
|
`legacy22-comparison.json` compare ces résultats aux rapports alpha.22 de
|
||||||
|
référence `build/alpha22-evidence/native22-create.json` et
|
||||||
|
`native22-reload.json`. Tous les champs de référence restent identiques, à
|
||||||
|
l’exception des identifiants de processus et chemins de mondes, volontairement
|
||||||
|
exclus. Les seules clés supplémentaires autorisées dans les nouveaux rapports
|
||||||
|
sont `transit_native_walk` et `hall_native_access` : ce sont des diagnostics
|
||||||
|
de parcours ajoutés au test, pas une modification du format de sauvegarde.
|
||||||
|
Ils passent aussi : les deux portiques sont reliés par un parcours de 548
|
||||||
|
pas de contrôle et le hall dispose d’un accès utilisable avec 22 blocs d’échelle
|
||||||
|
et huit ouvertures libres.
|
||||||
|
|
||||||
|
Le même reçu atteste que le preset conservé `sanctuary_v22.json` est identique
|
||||||
|
octet par octet à l’ancien preset `sanctuary.json` du commit `e654d56`.
|
||||||
|
Cette comparaison porte sur ce témoin de génération 22 créé et rechargé avec
|
||||||
|
le code actuel ; elle ne constitue ni une migration ni un essai de tous les
|
||||||
|
anciens mondes personnels.
|
||||||
|
|
||||||
|
### Réservations réelles d’expansion
|
||||||
|
|
||||||
|
Quatre appels réels à `ExpansionRuntime.Session.create` passent dans des JVM
|
||||||
|
neuves, sans aperçu d’expansion avant l’appel, sur des mondes de développement
|
||||||
|
de graine 0 et de génération 23. Chaque essai réussit dès le premier identifiant
|
||||||
|
`admission_23_0`, conserve le délai de recherche de production de 30 secondes
|
||||||
|
et termine avec une seule nouvelle île au statut `reserved`.
|
||||||
|
|
||||||
|
Les preuves sont dans `build/alpha231-evidence/admission-costs.json` et les
|
||||||
|
quatre rapports ci-dessous. La durée d’admission est celle de l’appel à
|
||||||
|
`Session.create` ; la durée du runner inclut le lancement et les contrôles.
|
||||||
|
|
||||||
|
| Demande | Preuve | Admission | Runner complet |
|
||||||
|
| --- | --- | ---: | ---: |
|
||||||
|
| 512, tempéré, naturel | `reserve-512-temperate-natural.json` | 206 ms | 53,62 s |
|
||||||
|
| 1 024, tempéré, naturel | `reserve-1024-temperate-natural.json` | 698 ms | 52,33 s |
|
||||||
|
| 1 024, tempéré, océan | `reserve-1024-temperate-ocean.json` | 441 ms | 53,40 s |
|
||||||
|
| 1 024, Lost City, naturel | `reserve-1024-lost_city-natural.json` | 12 849 ms | 64,15 s |
|
||||||
|
|
||||||
|
Le contrôle vérifie la garantie native appliquée par `Session.create`, la
|
||||||
|
relecture du journal durable, la conservation du profil capturé avant l’appel
|
||||||
|
et l’idempotence : répéter le même identifiant ne crée pas une seconde
|
||||||
|
réservation et ne réécrit pas le journal. Le profil initial de priorités est
|
||||||
|
vide dans ces quatre témoins ; ils ne couvrent pas encore son évolution après
|
||||||
|
le recensement d’un continent généré.
|
||||||
|
|
||||||
|
Pour Lost City, le journal
|
||||||
|
`build/alpha23.1-reserve-1024-lost_city-natural.log` atteste un plan accepté
|
||||||
|
comportant un quartier et un réseau souterrain ; cette planification prend
|
||||||
|
12 575 ms à l’intérieur de l’appel d’admission.
|
||||||
|
|
||||||
|
**Ces quatre essais sont des réservations uniquement.** Leurs reçus indiquent
|
||||||
|
`reserve_only: true` et `full_generation_checked: false`. Ils ne valident pas
|
||||||
|
la préparation complète, le passage à `ready`, les blocs posés en FULL ou leur
|
||||||
|
réouverture ; leurs durées ne sont donc pas des temps de génération de
|
||||||
|
continents. Aucune publication ni expansion dans un monde personnel n’est
|
||||||
|
attestée par ces essais.
|
||||||
|
|
||||||
|
### Cycle complet d’une expansion réelle
|
||||||
|
|
||||||
|
`build/alpha231-evidence/admission.json` atteste le passage complet de
|
||||||
|
`Session.create` à `ready`, processus 11335, sur un monde de graine 0 :
|
||||||
|
expansion tempérée naturelle de **256 blocs**, identifiant `admission_23_0`,
|
||||||
|
centre `2096 -160`, graine d’île `-6422943410799146666`. Cette taille est
|
||||||
|
distincte des réservations de 512 et 1 024 blocs mesurées plus haut.
|
||||||
|
|
||||||
|
La première demande est réservée en 1 034 ms sous le délai de 30 secondes.
|
||||||
|
La préparation prend 94 780 ms ; le journal `build/alpha23.1-admission.log`
|
||||||
|
confirme **418 chunks FULL** lorsque l’expansion devient prête. Le runner
|
||||||
|
complet termine avec un code de sortie nul en 160,01 secondes.
|
||||||
|
|
||||||
|
Un portail en ruine natif est effectivement généré au départ de chunk
|
||||||
|
`131 -10`. Le contrôle vérifie la couverture complète du recensement, la
|
||||||
|
présence de ce départ natif, le journal durable et l’idempotence avant et après
|
||||||
|
la préparation. Le profil capturé pour cette réservation reste vide ; après
|
||||||
|
génération, le recensement propose douze familles prioritaires pour les
|
||||||
|
recherches suivantes, sans réécrire le profil de l’île déjà créée.
|
||||||
|
|
||||||
|
Le reçu indique `reserve_only: false`, `census_verified: true`,
|
||||||
|
`idempotent: true` et `passed: true`. Ce terrain distant ne nécessite pas de
|
||||||
|
certificat de réutilisation de chunks du vide.
|
||||||
|
|
||||||
|
`admission-reload.json` confirme la réouverture réussie dans le processus
|
||||||
|
11614, distinct du processus 11335 de création, en 56,07 secondes de runner.
|
||||||
|
L’île est immédiatement retrouvée à l’état `ready`, sans nouvel appel de
|
||||||
|
création (`create_called: false`) ni redémarrage de sa préparation. Le test retrouve le journal à
|
||||||
|
l’identique, le descripteur et son profil, le départ natif et ses références,
|
||||||
|
ainsi que le recensement. La preuve reste limitée à ce cycle tempéré naturel
|
||||||
|
de 256 blocs ; les grandes réservations ne deviennent pas pour autant des
|
||||||
|
témoins FULL.
|
||||||
|
|
||||||
|
### Catalogue et structures natives en FULL
|
||||||
|
|
||||||
|
`build/alpha231-evidence/native-create.json` atteste la réussite de la création
|
||||||
|
native, processus 10548 : **206 chunks FULL contrôlés**, sur cinq témoins de
|
||||||
|
structures. La dernière entrée `native-create` de `postchecks.json` possède
|
||||||
|
un code de sortie nul, en 232,40 secondes de runner. Les assertions sont dans
|
||||||
|
[Native23WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Native23WorldGameTests.java).
|
||||||
|
Les témoins utilisent des îles et des priorités choisies pour exercer ces
|
||||||
|
structures ; ils ne constituent pas une mesure de leur fréquence naturelle.
|
||||||
|
|
||||||
|
Le catalogue contient exactement les **52 identifiants distincts** du registre
|
||||||
|
Minecraft 26.3-pre-2. Parmi eux, 46 sont classés admissibles dans l’Overworld ;
|
||||||
|
la cité ancienne reste réservée aux Cavernes, quatre entrées au Nether et la
|
||||||
|
cité de l’End à l’End. Cette concordance du catalogue ne signifie pas que les
|
||||||
|
52 structures ont été générées en FULL dans cet essai.
|
||||||
|
|
||||||
|
La vérification d’admission est rejouée avec le descripteur Lost City du témoin
|
||||||
|
de réservation précédent : le quartier et le réseau souterrain existent dans
|
||||||
|
le plan avant et après son insertion dans la liste des îles du générateur.
|
||||||
|
Le placement natif reste accepté lorsque leurs volumes de construction sont
|
||||||
|
pris en compte. Ce contrôle porte sur une ville réellement planifiée,
|
||||||
|
pas sur un plan vide ; il ne valide pas encore son quartier entier en FULL.
|
||||||
|
|
||||||
|
| Témoin FULL | Présences contrôlées après décoration |
|
||||||
|
| --- | --- |
|
||||||
|
| Forteresse (`stronghold`) | Salle de portail native, 12 cadres de portail de l’End, générateur de monstres et coffres. |
|
||||||
|
| Chambres des épreuves (`trial_chambers`) | Générateurs d’épreuves et coffres-forts natifs (`vault`). |
|
||||||
|
| Monument océanique | Salles immergées en prismarine, eau et trois grands gardiens chargés dans le monde ; eau de surface et roche sous le fond vérifiées au centre de l’île témoin. |
|
||||||
|
| Igloo | Neige, coffre du sous-sol natif et échelles de son puits. |
|
||||||
|
| Pyramide du désert | Grès, quatre coffres et neuf TNT du piège natif. |
|
||||||
|
|
||||||
|
Les compteurs bruts `full_blocks` parcourent les emprises des pièces, qui
|
||||||
|
peuvent se recouvrir : leurs valeurs ne forment pas un inventaire de blocs
|
||||||
|
uniques. Ainsi, la valeur 91 pour `vault` n’atteste pas 91 coffres-forts
|
||||||
|
distincts. Le tableau rapporte surtout les présences requises. Aucun combat,
|
||||||
|
déclenchement d’épreuve, ouverture de butin ou usage du portail n’a été joué.
|
||||||
|
|
||||||
|
Les départs gardent leur identité dans le registre, leur géométrie admise et
|
||||||
|
leurs données après sérialisation et lecture. Le monument est adapté au fond
|
||||||
|
océanique et son agencement intérieur se reconstruit avec la graine de l’île.
|
||||||
|
Pour les chambres des épreuves, seule la hauteur absolue de départ est adaptée :
|
||||||
|
les pools, processeurs, alias et contenus natifs restent identiques.
|
||||||
|
|
||||||
|
La localisation par les tags natifs de l’œil de l’Ender et des cartes de
|
||||||
|
monument, de chambres des épreuves et de pyramide trouve les vrais départs.
|
||||||
|
Une sélection mélangeant plusieurs types vanilla réussit également. La
|
||||||
|
recherche en lecture seule conserve les compteurs de références ; avec
|
||||||
|
`skipReferenced`, seul le départ gagnant reçoit une référence supplémentaire.
|
||||||
|
Ces recherches ne changent pas la liste des îles.
|
||||||
|
|
||||||
|
La présence des trois grands gardiens est vérifiée après chargement effectif
|
||||||
|
des entités des chunks du monument. Elle ne repose pas uniquement sur leur
|
||||||
|
NBT sauvegardé. La réouverture réussit aussi : `native-reload.json`, processus
|
||||||
|
10722, contrôle de nouveau 206 chunks FULL. Les données `full_tags` des cinq
|
||||||
|
départs, incluant leurs références, et les compteurs `full_blocks` sont
|
||||||
|
identiques entre création et réouverture ; les trois grands gardiens sont
|
||||||
|
présents après rechargement des entités. Cette preuve reste limitée aux cinq
|
||||||
|
témoins retenus.
|
||||||
|
|
||||||
|
### Six profils océaniques en FULL
|
||||||
|
|
||||||
|
`build/alpha231-evidence/ocean.json` atteste la réussite de la matrice
|
||||||
|
océanique, processus 11735, graine de monde 0, génération 23 : **34 chunks
|
||||||
|
échantillons demandés au statut FULL** pour six îles temporaires. Le runner
|
||||||
|
termine en 91,39 secondes, avec un code de sortie nul. Cette sélection couvre
|
||||||
|
un bassin profond, une rive et l’extérieur de chaque profil, ainsi que les
|
||||||
|
chunks voisins nécessaires aux témoins de glace ; elle ne génère pas la
|
||||||
|
totalité des six océans.
|
||||||
|
|
||||||
|
| Diamètre | Climat | Graine d’île | Eau à Y | Fond au point témoin | Biome profond stocké |
|
||||||
|
| --- | --- | ---: | ---: | ---: | --- |
|
||||||
|
| 512 | Glacial | 23 042 | 220 | 176 | `minecraft:deep_frozen_ocean` |
|
||||||
|
| 512 | Tempéré | 23 143 | 220 | 171 | `minecraft:deep_ocean` |
|
||||||
|
| 512 | Chaud | 23 244 | 220 | 176 | `minecraft:deep_lukewarm_ocean` |
|
||||||
|
| 1 024 | Glacial | 23 345 | 241 | 193 | `minecraft:deep_frozen_ocean` |
|
||||||
|
| 1 024 | Tempéré | 23 446 | 241 | 195 | `minecraft:deep_ocean` |
|
||||||
|
| 1 024 | Chaud | 23 547 | 241 | 198 | `minecraft:deep_lukewarm_ocean` |
|
||||||
|
|
||||||
|
Chaque profil conserve 16 appuis rocheux sur 16 et 16 échantillons immergés
|
||||||
|
sur 16, après décoration native. Les rives stockent `minecraft:snowy_beach`
|
||||||
|
pour les deux profils glaciaux et `minecraft:beach` pour les autres. Les
|
||||||
|
points extérieurs contrôlés restent entièrement dans l’air et le biome
|
||||||
|
`minecraft:the_void`.
|
||||||
|
|
||||||
|
Les deux profils glaciaux passent également un placement explicite des
|
||||||
|
features natives d’iceberg et de glace bleue. Avec la graine de feature
|
||||||
|
23 041, les sommets de glace atteignent respectivement Y 223 et Y 244,
|
||||||
|
au-dessus de leur niveau marin local. Le contexte de décoration de production
|
||||||
|
fournit ce niveau au générateur et à la région, conserve la protection du fond,
|
||||||
|
puis restaure le niveau global à −64. Ces placements explicites contrôlent
|
||||||
|
la compatibilité des features ; ils n’attestent pas une fréquence naturelle
|
||||||
|
d’icebergs dans toutes les graines.
|
||||||
|
|
||||||
|
L’ancien échec `ocean-ice-failure.json` utilisait un tirage d’iceberg pouvant
|
||||||
|
ne poser aucune glace dans le code vanilla. Le nouveau témoin déterministe
|
||||||
|
remplace ce tirage ; `ocean.json` constitue la preuve finale de cette suite.
|
||||||
|
La liste initiale des îles et les fichiers du journal sont inchangés après
|
||||||
|
les essais (`journal_unchanged: true`). **La réouverture des six profils dans
|
||||||
|
une autre JVM n’a pas été exécutée** ; le monument océanique dispose, lui,
|
||||||
|
de la preuve de réouverture décrite plus haut.
|
||||||
|
|
||||||
|
### Fabrication et distribution
|
||||||
|
|
||||||
|
`./gradlew check build` réussit en 3 min 48 s : 80 tâches, dont 73 exécutées.
|
||||||
|
Ses **24 GameTests** passent en 51,12 s, avec les régressions historiques,
|
||||||
|
le Blocodex, les priorités, le journal, le recensement et les protections.
|
||||||
|
`./gradlew assemblePack` réussit en 3 min 45 s : 81 tâches, dont 70 exécutées.
|
||||||
|
Cette seconde commande relance ses dépendances de vérification ; aucun échec.
|
||||||
|
Journaux : `build/alpha23.1-check-build.log` et
|
||||||
|
`build/alpha23.1-assemble-pack.log`.
|
||||||
|
|
||||||
|
L’export packwiz produit `build/Sanctuary-0.1.0-alpha.23.1.mrpack`.
|
||||||
|
Le contrôle des artefacts vérifie les versions, le contenu de l’index packwiz,
|
||||||
|
le JAR embarqué identique au JAR construit, l’absence de classes de test dans
|
||||||
|
le JAR et le bootstrap Prism officiel v0.0.3. Reçu :
|
||||||
|
`build/alpha231-evidence/delivery.json`.
|
||||||
|
|
||||||
|
| Artefact | SHA-256 |
|
||||||
|
| --- | --- |
|
||||||
|
| JAR Sanctuary alpha.23.1 | `906fd2e12be4233676a1a561556508eee7412183698c2cac704dc1191324f136` |
|
||||||
|
| MRpack alpha.23.1 | `d791025721ae52a3bb7b8d385fd5930cd4fc1a2dc9a4fa255ffb72a27a2455ad` |
|
||||||
|
|
||||||
|
Le MRpack est **local**, pas encore publié. Le gestionnaire d’identifiants Git
|
||||||
|
ne fournit aucun accès à `git.botsu.net` ; aucune release ni mise à jour du canal
|
||||||
|
n’a été effectuée. Le canal public a été relu à `0.1.0-alpha.22`.
|
||||||
|
L’instance Prism existante, ses sauvegardes et ses réglages n’ont pas été modifiés.
|
||||||
|
Les synchronisations réelles de l’installateur et de Prism restent à effectuer
|
||||||
|
après publication. Le ZIP d’amorçage préparé pointe vers ce canal stable ; il ne
|
||||||
|
constitue donc pas une installation de l’alpha.23.1 à ce stade.
|
||||||
|
|
||||||
|
La matrice océanique ne possède pas de réouverture complète ; le monument
|
||||||
|
océanique a son témoin de réouverture. Windows et le rendu client restent non testés.
|
||||||
|
|
||||||
|
## Reproduire les essais de secrets
|
||||||
|
|
||||||
|
Utiliser uniquement un dossier de monde de développement prévu pour la création
|
||||||
|
correspondante. Chaque nouvelle création doit disposer d’un monde de test neuf ;
|
||||||
|
une réouverture suit sa création avec la même graine, la même taille et les mêmes
|
||||||
|
options. Exécuter les JVM Gradle séquentiellement.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew :sanctuary:secrets23Smoke
|
||||||
|
./gradlew :sanctuary:runGameTest -PsanctuarySecrets23Tests=true -PsanctuaryDensityMaps=false -PsanctuaryTestSeed=42 -PsanctuaryTestDiameter=512
|
||||||
|
./gradlew :sanctuary:runGameTest -PsanctuarySecrets23Tests=true -PsanctuaryDensityMaps=false -PsanctuaryTestSeed=42 -PsanctuaryTestDiameter=512 -PsanctuaryExpansionReload=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tailles alternatives utilisent `724` et `1024`. Les scénarios OFF ajoutent
|
||||||
|
respectivement `-PsanctuaryExperimentalStructures=false` ou
|
||||||
|
`-PsanctuaryGenerateStructures=false`. Ces commandes décrivent les contrôles à
|
||||||
|
exécuter ; leur présence dans le rapport n’atteste pas leur réussite. Les rapports
|
||||||
|
originaux du runner sont dans
|
||||||
|
`mods/sanctuary/build/run/gameTest/diagnostics/secrets23-<graine>-<diamètre>/`.
|
||||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
|||||||
loom_version=1.17.20
|
loom_version=1.17.20
|
||||||
fabric_api_version=0.160.0+26.3
|
fabric_api_version=0.160.0+26.3
|
||||||
|
|
||||||
mod_version=0.1.0-alpha.23
|
mod_version=0.1.0-alpha.23.1
|
||||||
pack_version=0.1.0-alpha.23
|
pack_version=0.1.0-alpha.23.1
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
|
|||||||
@@ -40,7 +40,16 @@ tasks.named('runGameTest') {
|
|||||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
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' : '22'))
|
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' : '23'))
|
||||||
|
systemProperty('sanctuary.test.secrets23', providers.gradleProperty('sanctuarySecrets23Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.admission23', providers.gradleProperty('sanctuaryAdmission23Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.admission23.reserveOnly', providers.gradleProperty('sanctuaryAdmission23ReserveOnly').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.admission23.diameter', providers.gradleProperty('sanctuaryAdmission23Diameter').getOrElse('256'))
|
||||||
|
systemProperty('sanctuary.test.admission23.climate', providers.gradleProperty('sanctuaryAdmission23Climate').getOrElse('temperate'))
|
||||||
|
systemProperty('sanctuary.test.admission23.relief', providers.gradleProperty('sanctuaryAdmission23Relief').getOrElse('natural'))
|
||||||
|
systemProperty('sanctuary.test.ocean23', providers.gradleProperty('sanctuaryOcean23Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.native23', providers.gradleProperty('sanctuaryNative23Tests').getOrElse('false'))
|
||||||
|
systemProperty('sanctuary.test.generateStructures', providers.gradleProperty('sanctuaryGenerateStructures').getOrElse('true'))
|
||||||
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
||||||
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
|
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
|
||||||
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||||
@@ -85,6 +94,14 @@ tasks.named('processGametestResources') {
|
|||||||
inputs.property('heritage21', heritage21)
|
inputs.property('heritage21', heritage21)
|
||||||
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('lostCity21', lostCity21)
|
inputs.property('lostCity21', lostCity21)
|
||||||
|
def secrets23 = providers.gradleProperty('sanctuarySecrets23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
def admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('admission23', admission23)
|
||||||
|
def ocean23 = providers.gradleProperty('sanctuaryOcean23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('ocean23', ocean23)
|
||||||
|
def native23 = providers.gradleProperty('sanctuaryNative23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('secrets23', secrets23)
|
||||||
|
inputs.property('native23', native23)
|
||||||
def selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
|
def selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('selective22', selective22)
|
inputs.property('selective22', selective22)
|
||||||
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
|
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||||
@@ -94,7 +111,7 @@ tasks.named('processGametestResources') {
|
|||||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('voidReuse', voidReuse)
|
inputs.property('voidReuse', voidReuse)
|
||||||
filesMatching('fabric.mod.json') {
|
filesMatching('fabric.mod.json') {
|
||||||
expand(gametest_entrypoint: 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: 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'
|
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||||
@@ -548,3 +565,26 @@ tasks.named('lostCitySurface21Smoke') {
|
|||||||
}
|
}
|
||||||
tasks.named('check') { dependsOn(smoke) }
|
tasks.named('check') { dependsOn(smoke) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New geography has a pure harness in addition to native world witnesses.
|
||||||
|
tasks.register('expansionOcean23Smoke', JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
dependsOn('testClasses')
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
mainClass = 'fr.koka.sanctuary.expansion.ExpansionOcean23Smoke'
|
||||||
|
}
|
||||||
|
tasks.named('check') { dependsOn('expansionOcean23Smoke') }
|
||||||
|
|
||||||
|
[
|
||||||
|
secrets23Smoke: 'fr.koka.sanctuary.worldgen.mineral.Secrets23Smoke',
|
||||||
|
structureCensusSmoke: 'fr.koka.sanctuary.knowledge.structures.StructureCensusSmoke',
|
||||||
|
expansionPriorities23Smoke: 'fr.koka.sanctuary.expansion.ExpansionPriorities23Smoke'
|
||||||
|
].each { taskName, harness ->
|
||||||
|
tasks.register(taskName, JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
dependsOn('testClasses')
|
||||||
|
classpath = sourceSets.test.runtimeClasspath
|
||||||
|
mainClass = harness
|
||||||
|
}
|
||||||
|
tasks.named('check') { dependsOn(taskName) }
|
||||||
|
}
|
||||||
|
|||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
package fr.koka.sanctuary.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionAdmission;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionJournal;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionVoidCertificate;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionVoidReuse;
|
||||||
|
import fr.koka.sanctuary.expansion.NativeStructureMetadata23;
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import java.io.IOException;
|
||||||
|
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.concurrent.TimeUnit;
|
||||||
|
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** The real public admission transaction and preparation queue, with the production thirty-second deadline. */
|
||||||
|
public final class Expansion23AdmissionGameTests {
|
||||||
|
private static final int DISTANCE = 2048;
|
||||||
|
|
||||||
|
@GameTest(maxTicks = 24000)
|
||||||
|
public void createPreparePersistAndReload(GameTestHelper helper) throws Exception {
|
||||||
|
helper.assertTrue(Boolean.getBoolean("sanctuary.test.admission23"), "Admission fixture must be explicitly enabled");
|
||||||
|
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 23 && helper.getLevel().structureManager().shouldGenerateStructures(),
|
||||||
|
"Admission fixture requires generation23 with native structures enabled");
|
||||||
|
var scenario = new Scenario(helper, generator);
|
||||||
|
if (scenario.reload) scenario.verifyReload(); else scenario.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Scenario {
|
||||||
|
final GameTestHelper helper;
|
||||||
|
final ServerLevel level;
|
||||||
|
final SanctuaryChunkGenerator generator;
|
||||||
|
final ExpansionRuntime.Session session;
|
||||||
|
final boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||||
|
final boolean reserveOnly = Boolean.getBoolean("sanctuary.test.admission23.reserveOnly");
|
||||||
|
final int diameter = Integer.getInteger("sanctuary.test.admission23.diameter", 256);
|
||||||
|
final String climate = System.getProperty("sanctuary.test.admission23.climate", "temperate");
|
||||||
|
final String relief = System.getProperty("sanctuary.test.admission23.relief", "natural");
|
||||||
|
final Path world, directory, journal, proof;
|
||||||
|
final Map<String, Object> report = new LinkedHashMap<>();
|
||||||
|
final List<Map<String, Object>> attempts = new ArrayList<>();
|
||||||
|
ExpansionIsland reservation;
|
||||||
|
List<String> captured;
|
||||||
|
String certificate = "";
|
||||||
|
long preparationStarted;
|
||||||
|
boolean drained;
|
||||||
|
|
||||||
|
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||||
|
this.helper = helper; this.level = helper.getLevel(); this.generator = generator;
|
||||||
|
helper.assertTrue(!reload || !reserveOnly, "Reserve-only costs require a fresh JVM and disposable world, not reload mode");
|
||||||
|
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("admission23-create.json");
|
||||||
|
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world.toString());
|
||||||
|
report.put("world_seed", level.getSeed()); report.put("generation", generator.rootGeneration());
|
||||||
|
report.put("deadline_seconds", 30); report.put("attempts", attempts);
|
||||||
|
report.put("reserve_only", reserveOnly); report.put("requested_diameter", diameter);
|
||||||
|
report.put("requested_climate", climate); report.put("requested_relief", relief);
|
||||||
|
report.put("no_expansion_preview_before_create", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void create() throws Exception {
|
||||||
|
helper.assertTrue(!Files.exists(proof) && session.islands().size() == 1, "Use a new disposable world for the first admission pass");
|
||||||
|
long remaining = ExpansionAdmission.searchDeadline() - System.nanoTime();
|
||||||
|
helper.assertTrue(remaining > TimeUnit.SECONDS.toNanos(29) && remaining <= TimeUnit.SECONDS.toNanos(30),
|
||||||
|
"Fixture uses the unchanged production30s deadline");
|
||||||
|
byte[] untouchedJournal = Files.readAllBytes(journal);
|
||||||
|
var initialIslands = session.islands();
|
||||||
|
try {
|
||||||
|
for (int draw = 0; draw < 3 && reservation == null; draw++) {
|
||||||
|
String id = "admission_23_" + draw;
|
||||||
|
captured = StructureCensusService.priorities(level);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
try {
|
||||||
|
reservation = session.create(id, "sanctuary", "east", climate, diameter, relief, DISTANCE);
|
||||||
|
long elapsed = elapsedMillis(started);
|
||||||
|
attempts.add(Map.of("id", id, "result", "reserved", "milliseconds", elapsed));
|
||||||
|
helper.assertTrue(elapsed <= 31_000, "Real admission completes within its30s budget plus bounded clock/IO overhead");
|
||||||
|
} catch (IllegalArgumentException | IOException refused) {
|
||||||
|
attempts.add(Map.of("id", id, "result", String.valueOf(refused.getMessage()), "milliseconds", elapsedMillis(started)));
|
||||||
|
helper.assertTrue(session.islands().equals(initialIslands) && Arrays.equals(untouchedJournal, Files.readAllBytes(journal)),
|
||||||
|
"A refused request leaves no reservation or journal mutation");
|
||||||
|
// A timeout, IO failure or other implementation error is not a new random draw.
|
||||||
|
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.assertTrue(reservation != null, "A real expansion is admitted within three bounded feature refusals: " + diameter + " " + climate + " " + relief);
|
||||||
|
helper.assertTrue(reservation.status().equals("reserved") && session.islands().size() == 2,
|
||||||
|
"The transaction durably reserves exactly one child before preparation");
|
||||||
|
helper.assertTrue(reservation.structurePriorities().equals(captured), "The stored profile equals the snapshot taken before create");
|
||||||
|
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reservation.id()), "Fresh distant terrain prepares without a void-reuse reload");
|
||||||
|
certificate = certificate(reservation);
|
||||||
|
verifyJournal(reservation);
|
||||||
|
var snapshot = Files.readAllBytes(journal);
|
||||||
|
var repeated = session.create(reservation.id(), "sanctuary", "east", climate, diameter, relief, DISTANCE);
|
||||||
|
helper.assertTrue(repeated.equals(reservation) && Arrays.equals(snapshot, Files.readAllBytes(journal)) && session.islands().size() == 2,
|
||||||
|
"Repeating create on the reserved ID does not reserve again or rewrite the profile");
|
||||||
|
if (reserveOnly) {
|
||||||
|
report.put("island", encode(reservation)); report.put("captured_priorities", captured);
|
||||||
|
report.put("journal_sha256", ExpansionVoidCertificate.digest(Files.readString(journal)));
|
||||||
|
report.put("certificate", certificate); report.put("certificate_required", !reservation.reuseToken().isEmpty());
|
||||||
|
report.put("generator_codec", generatorCodec()); report.put("idempotent", true);
|
||||||
|
report.put("native_guarantee_checked_by_session_create", true);
|
||||||
|
report.put("full_generation_checked", false); report.put("passed", true);
|
||||||
|
write("reserve-" + diameter + "-" + climate + "-" + relief);
|
||||||
|
helper.succeed(); return;
|
||||||
|
}
|
||||||
|
preparationStarted = System.nanoTime();
|
||||||
|
helper.runAfterDelay(5, this::awaitReady);
|
||||||
|
} catch (Exception failure) { fail(failure); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void awaitReady() {
|
||||||
|
try {
|
||||||
|
var current = session.island(reservation.id());
|
||||||
|
if (!current.status().equals("ready")) {
|
||||||
|
helper.assertTrue(session.progress().matches(java.util.regex.Pattern.quote(reservation.id()) + ": \\d+/\\d+"),
|
||||||
|
"Real preparation must keep progressing, not stop silently: " + session.progress());
|
||||||
|
helper.runAfterDelay(5, this::awaitReady); return;
|
||||||
|
}
|
||||||
|
if (!drained) { drained = true; helper.runAfterDelay(5, this::awaitReady); return; }
|
||||||
|
helper.assertTrue(current.equals(reservation.ready()), "Preparation changes only reserved→ready, preserving identity and priorities");
|
||||||
|
verifyJournal(current);
|
||||||
|
helper.assertTrue(certificate.equals(certificate(current)), "Preparation cannot rewrite the certified terrain identity");
|
||||||
|
var starts = nativeStarts(current);
|
||||||
|
helper.assertTrue(!starts.isEmpty(), "An admitted expansion really creates at least one registered native start");
|
||||||
|
verifyCensus(current, starts);
|
||||||
|
report.put("priorities_after_generation", StructureCensusService.priorities(level));
|
||||||
|
byte[] readyJournal = Files.readAllBytes(journal);
|
||||||
|
helper.assertTrue(session.create(current.id(), "sanctuary", "east", current.climate(), current.diameter(), current.relief(), DISTANCE).equals(current),
|
||||||
|
"A ready ID stays idempotent even after census evidence has changed");
|
||||||
|
helper.assertTrue(Arrays.equals(readyJournal, Files.readAllBytes(journal)), "Idempotence preserves exact journal bytes");
|
||||||
|
StructureCensusService.flush(level.getServer());
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist actual FULL preparation and census before shutdown");
|
||||||
|
report.put("preparation_milliseconds", elapsedMillis(preparationStarted));
|
||||||
|
report.put("island", encode(current)); report.put("captured_priorities", captured);
|
||||||
|
report.put("journal_sha256", ExpansionVoidCertificate.digest(Files.readString(journal)));
|
||||||
|
report.put("certificate", certificate); report.put("certificate_required", !current.reuseToken().isEmpty());
|
||||||
|
report.put("generator_codec", generatorCodec()); report.put("native_starts", tags(starts));
|
||||||
|
report.put("native_count", starts.size()); report.put("census_verified", true); report.put("idempotent", true);
|
||||||
|
report.put("passed", true); write("create");
|
||||||
|
Files.writeString(proof, json()); helper.succeed();
|
||||||
|
} catch (Exception failure) { fail(failure); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyReload() throws Exception {
|
||||||
|
try {
|
||||||
|
var previous = JsonParser.parseString(Files.readString(proof)).getAsJsonObject();
|
||||||
|
helper.assertTrue(previous.get("passed").getAsBoolean(), "Reload requires a successful first admission pass");
|
||||||
|
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use another JVM");
|
||||||
|
helper.assertTrue(previous.get("world_directory").getAsString().equals(world.toString())
|
||||||
|
&& previous.get("world_seed").getAsLong() == level.getSeed(), "Reload preserves the same development world and seed");
|
||||||
|
var saved = ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, previous.get("island")).getOrThrow();
|
||||||
|
var current = session.island(saved.id());
|
||||||
|
helper.assertTrue(current.equals(saved) && current.status().equals("ready") && session.islands().size() == 2,
|
||||||
|
"Startup restores the ready child directly, without another create call");
|
||||||
|
helper.assertTrue(generator.source().islands().equals(session.islands()), "Loaded world generation uses the journal snapshot");
|
||||||
|
helper.assertTrue(ExpansionVoidCertificate.digest(Files.readString(journal)).equals(previous.get("journal_sha256").getAsString()),
|
||||||
|
"Opening a ready journal preserves its exact bytes");
|
||||||
|
verifyJournal(current);
|
||||||
|
helper.assertTrue(previous.get("generator_codec").equals(generatorCodec()), "The generator codec survives a full process restart");
|
||||||
|
helper.assertTrue(previous.get("certificate").getAsString().equals(certificate(current)), "The same certificate decision survives startup");
|
||||||
|
var starts = nativeStarts(current);
|
||||||
|
helper.assertTrue(previous.get("native_starts").equals(tags(starts)), "Native start identity, contents and reference counters survive reload");
|
||||||
|
verifyCensus(current, starts);
|
||||||
|
helper.assertTrue(session.progress().equals("idle"), "Ready regions do not restart their preparation job");
|
||||||
|
report.put("island", encode(current)); report.put("native_count", starts.size());
|
||||||
|
report.put("previous_process_id", previous.get("process_id").getAsLong());
|
||||||
|
report.put("create_called", false); report.put("census_verified", true); report.put("passed", true);
|
||||||
|
write("reload"); helper.succeed();
|
||||||
|
} catch (Exception failure) { fail(failure); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StructureStart> nativeStarts(ExpansionIsland island) {
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var starts = new ArrayList<StructureStart>();
|
||||||
|
for (var candidate : ExpansionStructures23.candidates(island)) {
|
||||||
|
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
for (var start : chunk.getAllStarts().values()) {
|
||||||
|
if (!start.isValid()) continue;
|
||||||
|
var id = registry.getKey(start.getStructure());
|
||||||
|
if (id != null && id.getNamespace().equals("minecraft")
|
||||||
|
&& ((NativeStructureMetadata23) (Object) start).sanctuary$native23() != null) starts.add(start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
starts.sort(Comparator.comparingInt((StructureStart s) -> s.getChunkPos().x()).thenComparingInt(s -> s.getChunkPos().z())
|
||||||
|
.thenComparing(s -> registry.getKey(s.getStructure()).toString()));
|
||||||
|
return List.copyOf(starts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyCensus(ExpansionIsland island, List<StructureStart> starts) {
|
||||||
|
var view = StructureCensusService.snapshot(level.getServer());
|
||||||
|
helper.assertTrue(view.available(), "Passive structure census stays available through actual generation and reload");
|
||||||
|
var region = view.regions().stream().filter(r -> r.island().equals(island.id())).findFirst().orElseThrow();
|
||||||
|
helper.assertTrue(region.coverage().startsComplete() && region.coverage().fullChunks() == region.coverage().expectedChunks(),
|
||||||
|
"The preparation queue certifies finite native-start and FULL coverage of the ready island");
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
for (var start : starts) {
|
||||||
|
String id = registry.getKey(start.getStructure()).toString();
|
||||||
|
var key = new StructureCensusMemory.SiteKey(level.dimension().identifier().toString(), id,
|
||||||
|
start.getChunkPos().x(), 0, start.getChunkPos().z(), false);
|
||||||
|
var indexed = view.evidence().sites().get(key);
|
||||||
|
helper.assertTrue(indexed != null && view.evidence().generated(indexed), "The census records the real generated occurrence: " + id);
|
||||||
|
var family = region.families().get(StructureCensusService.family(id));
|
||||||
|
helper.assertTrue(family != null && family.presence() == StructureCensusService.Presence.PRESENT && family.generated() > 0,
|
||||||
|
"A generated native family is present, never absent or unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyJournal(ExpansionIsland island) throws IOException {
|
||||||
|
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, encode(island)).getOrThrow().equals(island), "Every descriptor field survives CODEC roundtrip");
|
||||||
|
var reopened = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
|
||||||
|
helper.assertTrue(reopened.islands().equals(session.islands()) && reopened.islands().contains(island),
|
||||||
|
"A separate read of the durable journal agrees with the live session");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String certificate(ExpansionIsland island) throws IOException {
|
||||||
|
if (island.reuseToken().isEmpty()) return ""; // Fully absent terrain needs no void-reuse certificate.
|
||||||
|
String text = Files.readString(directory.resolve("void-reuse").resolve(island.reuseToken() + ".json"));
|
||||||
|
helper.assertTrue(ExpansionVoidCertificate.digest(text).equals(island.reuseToken()), "Certificate content matches its immutable token");
|
||||||
|
var body = JsonParser.parseString(text).getAsJsonObject();
|
||||||
|
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, body.get("island")).getOrThrow().equals(island.withReuseToken("").ready()),
|
||||||
|
"Certificate binds the same immutable descriptor and priority profile");
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonArray tags(List<StructureStart> starts) {
|
||||||
|
var result = new JsonArray(); var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||||
|
for (var start : starts) {
|
||||||
|
var saved = new JsonObject(); saved.addProperty("id", registry.getKey(start.getStructure()).toString());
|
||||||
|
saved.addProperty("x", start.getChunkPos().x()); saved.addProperty("z", start.getChunkPos().z());
|
||||||
|
saved.addProperty("nbt", start.createTag(context, start.getChunkPos()).toString()); result.add(saved);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private com.google.gson.JsonElement encode(ExpansionIsland island) { return ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, island).getOrThrow(); }
|
||||||
|
private com.google.gson.JsonElement generatorCodec() {
|
||||||
|
return ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
|
||||||
|
}
|
||||||
|
private String json() { return new GsonBuilder().setPrettyPrinting().create().toJson(report); }
|
||||||
|
private void write(String name) throws IOException {
|
||||||
|
var output = Path.of("diagnostics", "admission23"); Files.createDirectories(output); Files.writeString(output.resolve(name + ".json"), json());
|
||||||
|
}
|
||||||
|
private void fail(Exception failure) {
|
||||||
|
report.put("passed", false); report.put("failure", failure.toString()); report.put("progress", session.progress());
|
||||||
|
try { write("failure"); } catch (IOException ignored) {}
|
||||||
|
throw new IllegalStateException("Production admission23 failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static long elapsedMillis(long started) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started); }
|
||||||
|
}
|
||||||
+314
@@ -0,0 +1,314 @@
|
|||||||
|
package fr.koka.sanctuary.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import fr.koka.sanctuary.expansion.*;
|
||||||
|
import fr.koka.sanctuary.mixin.MonumentChildren23Accessor;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
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.tags.StructureTags;
|
||||||
|
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.structure.Structure;
|
||||||
|
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.OceanMonumentPieces;
|
||||||
|
|
||||||
|
/** Exact native catalogue and pure admission, followed by real native start serialization in a disposable test world. */
|
||||||
|
public final class Native23WorldGameTests {
|
||||||
|
@GameTest(maxTicks = 18000)
|
||||||
|
public void nativeCatalogueAdmissionAndPersistence(GameTestHelper helper) throws Exception {
|
||||||
|
try { runNativeLifecycle(helper); }
|
||||||
|
catch (Exception failure) { writeFailure(helper, Map.of(), failure); throw failure; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runNativeLifecycle(GameTestHelper helper) throws Exception {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 23, "Requires generation23 disposable development world");
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var catalogue = ExpansionStructures23.catalogue();
|
||||||
|
helper.assertTrue(catalogue.size() == 52 && catalogue.stream().map(ExpansionStructureCatalog23.Entry::id).distinct().count() == 52, "All52 distinct built-in structures are classified");
|
||||||
|
helper.assertTrue(catalogue.stream().filter(e -> e.availability() == ExpansionStructureCatalog23.Availability.OVERWORLD).count() == 46, "46 admissible Overworld structures; ancient city awaits Cavernes");
|
||||||
|
for (var entry : catalogue) helper.assertTrue(registry.get(Identifier.parse(entry.id())).isPresent(), "Exact26.3-pre2 registry contains " + entry.id());
|
||||||
|
var nativeIds = new HashSet<String>();
|
||||||
|
registry.listElements().forEach(h -> { if (h.key().identifier().getNamespace().equals("minecraft")) nativeIds.add(h.key().identifier().toString()); });
|
||||||
|
helper.assertTrue(nativeIds.equals(catalogue.stream().map(ExpansionStructureCatalog23.Entry::id).collect(java.util.stream.Collectors.toSet())), "Catalogue exactly matches native registry, without omitted structures");
|
||||||
|
helper.assertTrue(ExpansionStructures23.family("other:custom").equals("unknown"), "Modded structures stay independently identifiable");
|
||||||
|
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||||
|
Structure trial = registry.getValue(Identifier.withDefaultNamespace("trial_chambers"));
|
||||||
|
var original = Structure.DIRECT_CODEC.encodeStart(ops, trial).getOrThrow().getAsJsonObject();
|
||||||
|
var moved = Structure.DIRECT_CODEC.encodeStart(ops, ExpansionStructures23.withStartHeight(trial, level.registryAccess(), 172)).getOrThrow().getAsJsonObject();
|
||||||
|
var originalUnchanged = original.deepCopy(); originalUnchanged.remove("start_height"); moved.remove("start_height");
|
||||||
|
helper.assertTrue(originalUnchanged.equals(moved), "Only absolute start_height changes; pools, processors, aliases, contents stay native");
|
||||||
|
helper.assertTrue(original.equals(Structure.DIRECT_CODEC.encodeStart(ops, trial).getOrThrow()), "Registered trial structure is not mutated");
|
||||||
|
var report = new LinkedHashMap<String, Object>();
|
||||||
|
report.put("catalogue", catalogue); report.put("generation", generator.rootGeneration()); report.put("world_seed", level.getSeed());
|
||||||
|
var prior = generator.source().islands();
|
||||||
|
ExpansionIsland witness = null;
|
||||||
|
for (int seed = 1; seed <= 8 && witness == null; seed++) {
|
||||||
|
var candidate = island("admission", 512, seed, "natural").withStructurePriorities(List.of("camp"));
|
||||||
|
try { ExpansionStructures23.verifyExpansion(level, candidate, deadline(45)); witness = candidate; }
|
||||||
|
catch (ExpansionStructures23.NoSuitableStructureException refused) { /* Try a different immutable candidate. */ }
|
||||||
|
}
|
||||||
|
helper.assertTrue(witness != null, "At least one native structure can be admitted on a temperate expansion");
|
||||||
|
helper.assertTrue(prior.equals(generator.source().islands()), "Preflight never activates or mutates the island snapshot");
|
||||||
|
var nodes = ExpansionStructures23.candidates(witness);
|
||||||
|
helper.assertTrue(nodes.equals(ExpansionStructures23.candidates(witness.ready())), "Candidate anchors do not depend on reserved/ready state");
|
||||||
|
var emitted = new ArrayList<StructureStart>();
|
||||||
|
try {
|
||||||
|
var published = new ArrayList<>(prior); published.add(witness.ready()); generator.setIslands(published);
|
||||||
|
for (var node : nodes) {
|
||||||
|
var proto = new ProtoChunk(node.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||||
|
ExpansionStructures23.generate(generator, level.registryAccess(), null, level.structureManager(), proto,
|
||||||
|
level.getStructureTemplateManager(), level.dimension());
|
||||||
|
for (StructureStart start : proto.getAllStarts().values()) if (start.isValid()) emitted.add(start);
|
||||||
|
if (!emitted.isEmpty()) break;
|
||||||
|
}
|
||||||
|
} finally { generator.setIslands(prior); }
|
||||||
|
helper.assertTrue(!emitted.isEmpty(), "Accepted preflight replays as a native start through production generate");
|
||||||
|
var serialization = StructurePieceSerializationContext.fromLevel(level);
|
||||||
|
for (StructureStart start : emitted) assertRoundTrip(helper, start, serialization);
|
||||||
|
report.put("guaranteed_island", witness); report.put("guaranteed_start", emitted.getFirst().createTag(serialization, emitted.getFirst().getChunkPos()).toString());
|
||||||
|
// A not-yet-published Lost City already reserves its complete underground/support envelope.
|
||||||
|
// Exact descriptor admitted by the separate cold Session.create1024 Lost City witness.
|
||||||
|
var cityWitness = new ExpansionIsland("admission_23_0", "sanctuary", "east", "lost_city", 1024,
|
||||||
|
2416, 304, -6422943410799146666L, "natural", "reserved", 23);
|
||||||
|
ExpansionStructures23.verifyExpansion(level, cityWitness, ExpansionAdmission.searchDeadline());
|
||||||
|
helper.assertTrue(!generator.lostCityPlan(cityWitness).isEmpty() && !generator.undergroundPlan16(cityWitness).networks().isEmpty(),
|
||||||
|
"Unpublished witness has a real city district and underground network before native admission replay");
|
||||||
|
boolean cityEmitted = false;
|
||||||
|
try {
|
||||||
|
var published = new ArrayList<>(prior); published.add(cityWitness.ready()); generator.setIslands(published);
|
||||||
|
helper.assertTrue(!generator.lostCityPlan(cityWitness).isEmpty() && !generator.undergroundPlan16(cityWitness).networks().isEmpty(),
|
||||||
|
"Publishing keeps the nonempty city and underground construction mask");
|
||||||
|
for (var node : ExpansionStructures23.candidates(cityWitness)) {
|
||||||
|
var proto = new ProtoChunk(node.chunk(), UpgradeData.EMPTY, level, PalettedContainerFactory.create(level.registryAccess()), null);
|
||||||
|
ExpansionStructures23.generate(generator, level.registryAccess(), null, level.structureManager(), proto, level.getStructureTemplateManager(), level.dimension());
|
||||||
|
if (proto.getAllStarts().values().stream().anyMatch(StructureStart::isValid)) { cityEmitted = true; break; }
|
||||||
|
}
|
||||||
|
} finally { generator.setIslands(prior); }
|
||||||
|
helper.assertTrue(cityEmitted, "Lost City preflight remains valid after its full construction mask is published");
|
||||||
|
report.put("lost_city_guaranteed_island", cityWitness);
|
||||||
|
helper.assertTrue(registry.getOrThrow(StructureTags.EYE_OF_ENDER_LOCATED).contains(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow()), "Eye of Ender uses native stronghold tag");
|
||||||
|
helper.assertTrue(registry.getOrThrow(StructureTags.ON_OCEAN_MONUMENT_MAPS).contains(registry.get(Identifier.withDefaultNamespace("monument")).orElseThrow()), "Explorer maps keep the native monument identity");
|
||||||
|
var witnesses = new ArrayList<ExpansionIsland>();
|
||||||
|
var selectedStarts = new ArrayList<ExpansionStructures23.Accepted>();
|
||||||
|
Path proof = level.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("native23-full-create.json");
|
||||||
|
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||||
|
var previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
|
||||||
|
for (String id : List.of("stronghold", "trial_chambers", "monument", "igloo", "desert_pyramid")) {
|
||||||
|
ExpansionStructures23.Accepted accepted = null;
|
||||||
|
ExpansionIsland acceptedIsland = null;
|
||||||
|
for (int seed = 1; seed <= 16 && accepted == null; seed++) {
|
||||||
|
var candidate = reload ? new GsonBuilder().create().fromJson(previous.getAsJsonObject(id).get("island"), ExpansionIsland.class)
|
||||||
|
: island("native_" + id, 1024, seed * 101L + 37, id.equals("monument") ? "ocean" : "natural")
|
||||||
|
.withStructurePriorities(List.of(id));
|
||||||
|
accepted = ExpansionStructures23.previewSelected(level, candidate, "minecraft:" + id, deadline(90));
|
||||||
|
if (accepted != null && id.equals("igloo") && accepted.start().getPieces().size() == 1) accepted = null;
|
||||||
|
if (accepted != null) acceptedIsland = candidate;
|
||||||
|
if (reload) break;
|
||||||
|
}
|
||||||
|
helper.assertTrue(accepted != null, "Native " + id + " has an admissible site in floating terrain");
|
||||||
|
assertRoundTrip(helper, accepted.start(), serialization);
|
||||||
|
helper.assertTrue(accepted.start().getStructure() == registry.getValue(Identifier.withDefaultNamespace(id)), "Saved structure remains original registry object");
|
||||||
|
if (id.equals("stronghold")) helper.assertTrue(accepted.start().getPieces().stream().anyMatch(p -> p.getClass().getSimpleName().equals("PortalRoom")), "Native stronghold retains its End portal room");
|
||||||
|
if (id.equals("monument")) {
|
||||||
|
helper.assertTrue(accepted.start().getBoundingBox().minY() != 39, "Monument moved to real expansion seabed");
|
||||||
|
var loaded = StructureStart.loadStaticStart(serialization, accepted.start().createTag(serialization, accepted.start().getChunkPos()), level.getSeed());
|
||||||
|
var before = ((MonumentChildren23Accessor) accepted.start().getPieces().getFirst()).sanctuary$children();
|
||||||
|
var after = ((MonumentChildren23Accessor) loaded.getPieces().getFirst()).sanctuary$children();
|
||||||
|
helper.assertTrue(before.size() == after.size(), "Monument room graph reconstructed with island seed, not world seed");
|
||||||
|
for (int i = 0; i < before.size(); i++) helper.assertTrue(before.get(i).getBoundingBox().equals(after.get(i).getBoundingBox())
|
||||||
|
&& before.get(i).getClass().equals(after.get(i).getClass()), "Internal monument pieces preserve altitude and layout on reload");
|
||||||
|
}
|
||||||
|
report.put(id, Map.of("island", acceptedIsland, "tag", accepted.start().createTag(serialization, accepted.start().getChunkPos()).toString()));
|
||||||
|
witnesses.add(acceptedIsland.ready()); selectedStarts.add(accepted);
|
||||||
|
}
|
||||||
|
helper.assertTrue(NativeStructurePlacement23.seaLevel() == null, "No placement context leaks out of native generation");
|
||||||
|
var output = Path.of("diagnostics", "native23"); Files.createDirectories(output);
|
||||||
|
report.put("admission_passed", true);
|
||||||
|
Files.writeString(output.resolve("admission.json"), new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
|
var published = new ArrayList<>(prior); published.addAll(witnesses); generator.setIslands(published);
|
||||||
|
var fullChunks = new java.util.TreeSet<net.minecraft.world.level.ChunkPos>(java.util.Comparator.comparingInt(net.minecraft.world.level.ChunkPos::x).thenComparingInt(net.minecraft.world.level.ChunkPos::z));
|
||||||
|
for (var selected : selectedStarts) selected.start().getBoundingBox().intersectingChunks().forEach(fullChunks::add);
|
||||||
|
var iterator = fullChunks.iterator();
|
||||||
|
var monument = selectedStarts.stream().filter(s -> s.structure().unwrapKey().orElseThrow().identifier().getPath().equals("monument")).findFirst().orElseThrow().start();
|
||||||
|
var entityChunks = monument.getBoundingBox().intersectingChunks().toList();
|
||||||
|
boolean[] entityTickets = {false};
|
||||||
|
int[] entityWait = {0};
|
||||||
|
Runnable releaseEntityTickets = () -> {
|
||||||
|
if (!entityTickets[0]) return;
|
||||||
|
for (var pos : entityChunks) level.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON, pos, 2);
|
||||||
|
entityTickets[0] = false;
|
||||||
|
};
|
||||||
|
Runnable[] step = new Runnable[1];
|
||||||
|
step[0] = () -> {
|
||||||
|
try {
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
var chunk = iterator.next(); level.getChunk(chunk.x(), chunk.z(), net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true);
|
||||||
|
// GameTest removes the executing Runnable from its map; enqueue a fresh callback.
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||||
|
}
|
||||||
|
if (!entityTickets[0]) {
|
||||||
|
report.put("guardians_before_entity_tickets", guardianDiagnostics(level, monument, entityChunks, 0));
|
||||||
|
// FULL's short loading ticket does not keep its saved entities visible. This native
|
||||||
|
// ticket loads and simulates without persisting, and is released on every exit.
|
||||||
|
for (var pos : entityChunks) level.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON, pos, 2);
|
||||||
|
entityTickets[0] = true;
|
||||||
|
}
|
||||||
|
var guardianState = guardianDiagnostics(level, monument, entityChunks, entityWait[0]);
|
||||||
|
report.put("guardian_diagnostics", guardianState);
|
||||||
|
if (((Number) guardianState.get("count")).intValue() < 3 && entityWait[0]++ < 200) {
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||||
|
}
|
||||||
|
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("Native23 guardian visibility: {}", guardianState);
|
||||||
|
var fullTags = new LinkedHashMap<String, String>();
|
||||||
|
var blocks = new LinkedHashMap<String, Map<String, Integer>>();
|
||||||
|
report.put("full_chunks", fullChunks.size()); report.put("full_blocks", blocks);
|
||||||
|
for (var selected : selectedStarts) {
|
||||||
|
var expected = selected.start(); var position = expected.getChunkPos();
|
||||||
|
var chunk = level.getChunk(position.x(), position.z(), net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true);
|
||||||
|
var actual = chunk.getStartForStructure(selected.structure().value());
|
||||||
|
helper.assertTrue(actual != null && actual.isValid(), "FULL production preserves the naturally selected structure");
|
||||||
|
helper.assertTrue(actual.getBoundingBox().equals(expected.getBoundingBox()), "FULL lifecycle preserves admitted geometry");
|
||||||
|
assertRoundTrip(helper, actual, serialization);
|
||||||
|
String id = selected.structure().unwrapKey().orElseThrow().identifier().getPath();
|
||||||
|
String saved = actual.createTag(serialization, position).toString();
|
||||||
|
if (reload) helper.assertTrue(previous.getAsJsonObject("full_tags").get(id).getAsString().equals(saved), "New JVM restores " + id + " with unchanged start and references");
|
||||||
|
var counts = countImportantBlocks(level, actual); blocks.put(id, counts);
|
||||||
|
if (id.equals("stronghold")) helper.assertTrue(counts.getOrDefault("end_portal_frame", 0) >= 12 && counts.getOrDefault("spawner", 0) >= 1 && counts.getOrDefault("chest", 0) >= 1,
|
||||||
|
"Stronghold postProcess creates usable portal frames, spawner and unopened loot chests");
|
||||||
|
if (id.equals("trial_chambers")) helper.assertTrue(counts.getOrDefault("trial_spawner", 0) > 0 && counts.getOrDefault("vault", 0) > 0,
|
||||||
|
"Trial chambers keep native trial spawners and vaults");
|
||||||
|
if (id.equals("igloo")) helper.assertTrue(counts.getOrDefault("ladder", 0) > 0 && counts.getOrDefault("chest", 0) > 0 && counts.getOrDefault("snow_block", 0) > 0,
|
||||||
|
"Projected igloo retains aligned entrance, shaft and native laboratory chest");
|
||||||
|
if (id.equals("desert_pyramid")) helper.assertTrue(counts.getOrDefault("sandstone", 0) > 50 && counts.getOrDefault("chest", 0) >= 4 && counts.getOrDefault("tnt", 0) >= 9,
|
||||||
|
"Projected desert pyramid retains its four loot chests and buried native trap");
|
||||||
|
if (id.equals("monument")) {
|
||||||
|
helper.assertTrue(counts.getOrDefault("prismarine", 0) > 100 && counts.getOrDefault("water", 0) > 100, "Monument has real submerged rooms after FEATURES");
|
||||||
|
int guardians = level.getEntitiesOfClass(net.minecraft.world.entity.monster.ElderGuardian.class,
|
||||||
|
net.minecraft.world.phys.AABB.of(actual.getBoundingBox())).size();
|
||||||
|
helper.assertTrue(guardians >= 3, "Native monument creates its three elder guardians");
|
||||||
|
counts.put("elder_guardians", guardians);
|
||||||
|
var oceanIsland = witnesses.stream().filter(i -> i.id().equals("native_monument")).findFirst().orElseThrow();
|
||||||
|
var ocean = ExpansionOcean23.column(oceanIsland, oceanIsland.centerX(), oceanIsland.centerZ());
|
||||||
|
helper.assertTrue(level.getBlockState(new net.minecraft.core.BlockPos(oceanIsland.centerX(), ocean.waterLevel(), oceanIsland.centerZ())).is(net.minecraft.world.level.block.Blocks.WATER), "Ocean surface remains water after features");
|
||||||
|
helper.assertTrue(level.getBlockState(new net.minecraft.core.BlockPos(oceanIsland.centerX(), ocean.floorY() - 3, oceanIsland.centerZ())).isSolid(), "Ocean retains a sealed geological floor after features");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verifyLocator(helper, generator, witnesses, selectedStarts, reload);
|
||||||
|
for (var selected : selectedStarts) {
|
||||||
|
var pos = selected.start().getChunkPos(); var actual = level.getChunk(pos.x(), pos.z(), net.minecraft.world.level.chunk.status.ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(selected.structure().value());
|
||||||
|
fullTags.put(selected.structure().unwrapKey().orElseThrow().identifier().getPath(), actual.createTag(serialization, pos).toString());
|
||||||
|
}
|
||||||
|
report.put("process_id", ProcessHandle.current().pid()); report.put("full_chunks", fullChunks.size()); report.put("full_blocks", blocks); report.put("full_tags", fullTags);
|
||||||
|
report.put("passed", true);
|
||||||
|
if (reload) helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload is a different JVM");
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist disposable full chunks and native starts");
|
||||||
|
var json = new GsonBuilder().setPrettyPrinting().create().toJson(report);
|
||||||
|
Files.writeString(output.resolve(reload ? "reload.json" : "full.json"), json);
|
||||||
|
if (!reload) Files.writeString(proof, json);
|
||||||
|
releaseEntityTickets.run(); generator.setIslands(prior); helper.succeed();
|
||||||
|
} catch (Exception failure) {
|
||||||
|
writeFailure(helper, report, failure);
|
||||||
|
releaseEntityTickets.run(); generator.setIslands(prior); throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> guardianDiagnostics(net.minecraft.server.level.ServerLevel level, StructureStart monument,
|
||||||
|
List<net.minecraft.world.level.ChunkPos> chunks, int waitedTicks) {
|
||||||
|
var guardians = level.getEntitiesOfClass(net.minecraft.world.entity.monster.ElderGuardian.class, net.minecraft.world.phys.AABB.of(monument.getBoundingBox()));
|
||||||
|
var state = new LinkedHashMap<String, Object>();
|
||||||
|
state.put("count", guardians.size()); state.put("difficulty", level.getDifficulty().toString()); state.put("waited_ticks", waitedTicks);
|
||||||
|
state.put("positions", guardians.stream().map(e -> List.of(e.getX(), e.getY(), e.getZ())).toList());
|
||||||
|
state.put("chunks", chunks.stream().map(p -> Map.of("x", p.x(), "z", p.z(),
|
||||||
|
"full_loaded", level.getChunkSource().getChunkNow(p.x(), p.z()) != null,
|
||||||
|
"entities_loaded", level.areEntitiesLoaded(p.pack()), "entities_ticking", level.areEntitiesActuallyLoadedAndTicking(p))).toList());
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFailure(GameTestHelper helper, Map<String, Object> partial, Exception failure) {
|
||||||
|
try {
|
||||||
|
var report = new LinkedHashMap<String, Object>(partial);
|
||||||
|
report.put("passed", false); report.put("failure_class", failure.getClass().getName()); report.put("failure", String.valueOf(failure.getMessage()));
|
||||||
|
report.put("difficulty", helper.getLevel().getDifficulty().toString()); report.put("process_id", ProcessHandle.current().pid());
|
||||||
|
var output = Path.of("diagnostics", "native23"); Files.createDirectories(output);
|
||||||
|
Files.writeString(output.resolve("failure.json"), new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
|
} catch (Exception diagnosticFailure) { failure.addSuppressed(diagnosticFailure); }
|
||||||
|
}
|
||||||
|
private static ExpansionIsland island(String id, int diameter, long seed, String relief) {
|
||||||
|
int x = switch (id) { case "native_stronghold" -> 8192; case "native_trial_chambers" -> 12288; case "native_monument" -> 16384;
|
||||||
|
case "native_igloo" -> 28672; case "native_desert_pyramid" -> 32768; default -> 4096; };
|
||||||
|
String climate = id.equals("native_igloo") ? "glacial" : id.equals("native_desert_pyramid") ? "arid" : "temperate";
|
||||||
|
return new ExpansionIsland(id, "sanctuary", "east", climate, diameter, x, 4096, seed, relief, "reserved", 23);
|
||||||
|
}
|
||||||
|
private static Map<String, Integer> countImportantBlocks(net.minecraft.server.level.ServerLevel level, StructureStart start) {
|
||||||
|
var counts = new LinkedHashMap<String, Integer>();
|
||||||
|
var important = java.util.Set.of("end_portal_frame", "spawner", "chest", "trial_spawner", "vault", "prismarine", "water", "wet_sponge", "ladder", "snow_block", "sandstone", "tnt");
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
var box = piece.getBoundingBox();
|
||||||
|
var pos = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||||
|
for (int z = box.minZ(); z <= box.maxZ(); z++) for (int x = box.minX(); x <= box.maxX(); x++) for (int y = box.minY() - (start.getStructure() instanceof net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure ? 16 : 0); y <= box.maxY(); y++) {
|
||||||
|
var state = level.getBlockState(pos.set(x, y, z));
|
||||||
|
String id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||||
|
if (important.contains(id)) counts.merge(id, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
private static void verifyLocator(GameTestHelper helper, SanctuaryChunkGenerator generator, List<ExpansionIsland> islands,
|
||||||
|
List<ExpansionStructures23.Accepted> selected, boolean reload) {
|
||||||
|
var level = helper.getLevel(); var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var beforeSnapshot = generator.source().islands();
|
||||||
|
for (var chosen : selected) {
|
||||||
|
var id = chosen.structure().unwrapKey().orElseThrow().identifier().getPath();
|
||||||
|
if (id.equals("igloo")) continue; // No built-in igloo explorer-map tag.
|
||||||
|
var tag = id.equals("desert_pyramid") ? StructureTags.ON_DESERT_PYRAMID_MAPS : id.equals("stronghold") ? StructureTags.EYE_OF_ENDER_LOCATED : id.equals("monument") ? StructureTags.ON_OCEAN_MONUMENT_MAPS : StructureTags.ON_BURIED_TRIAL_CHAMBERS_MAPS;
|
||||||
|
var located = generator.findNearestMapStructure(level, registry.getOrThrow(tag), chosen.start().getPieces().getFirst().getLocatorPosition(), 8, false);
|
||||||
|
helper.assertTrue(located != null && located.getSecond().equals(chosen.structure()), "Native tag locator finds actual " + id + " starts");
|
||||||
|
}
|
||||||
|
var tracked = new ArrayList<StructureStart>();
|
||||||
|
for (var island : islands) for (var candidate : ExpansionStructures23.candidates(island)) {
|
||||||
|
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), net.minecraft.world.level.chunk.status.ChunkStatus.STRUCTURE_STARTS, true);
|
||||||
|
chunk.getAllStarts().values().stream().filter(StructureStart::isValid).forEach(tracked::add);
|
||||||
|
}
|
||||||
|
var counts = new java.util.IdentityHashMap<StructureStart, Integer>(); tracked.forEach(s -> counts.put(s, s.getReferences()));
|
||||||
|
var wanted = HolderSet.direct(selected.stream().map(ExpansionStructures23.Accepted::structure).toList());
|
||||||
|
var origin = selected.getFirst().start().getPieces().getFirst().getLocatorPosition();
|
||||||
|
var found = generator.findNearestMapStructure(level, wanted, origin, 128, false);
|
||||||
|
helper.assertTrue(found != null, "Mixed vanilla holder sets locate a real start");
|
||||||
|
helper.assertTrue(tracked.stream().allMatch(s -> counts.get(s) == s.getReferences()), "Read-only locate consumes no references");
|
||||||
|
if (!reload) {
|
||||||
|
found = generator.findNearestMapStructure(level, wanted, origin, 128, true);
|
||||||
|
helper.assertTrue(found != null, "skipReferenced locates an unreferenced winner");
|
||||||
|
int changed = 0, increment = 0;
|
||||||
|
for (var start : tracked) { int delta = start.getReferences() - counts.get(start); if (delta != 0) changed++; increment += delta; }
|
||||||
|
helper.assertTrue(changed == 1 && increment == 1, "Exactly one winning start gains one reference");
|
||||||
|
}
|
||||||
|
helper.assertTrue(beforeSnapshot.equals(generator.source().islands()), "Tags, maps and locator never activate another expansion");
|
||||||
|
}
|
||||||
|
private static long deadline(int seconds) { return System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(seconds); }
|
||||||
|
private static void assertRoundTrip(GameTestHelper helper, StructureStart start, StructurePieceSerializationContext context) {
|
||||||
|
var tag = start.createTag(context, start.getChunkPos());
|
||||||
|
var loaded = StructureStart.loadStaticStart(context, tag, helper.getLevel().getSeed());
|
||||||
|
helper.assertTrue(loaded != null && loaded.isValid() && loaded.getBoundingBox().equals(start.getBoundingBox()), "Native start bounds survive reload");
|
||||||
|
helper.assertTrue(tag.equals(loaded.createTag(context, start.getChunkPos())), "Native start metadata and content survive reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
+243
@@ -0,0 +1,243 @@
|
|||||||
|
package fr.koka.sanctuary.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionOcean23;
|
||||||
|
import fr.koka.sanctuary.expansion.NativeStructurePlacement23;
|
||||||
|
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.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.server.level.GenerationChunkHolder;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.tags.FluidTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.util.StaticCache2D;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkPyramid;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.BlueIceFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.IcebergFeature;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
||||||
|
/** Full native decoration on six disposable ocean islands; this fixture never reserves an expansion. */
|
||||||
|
public final class Ocean23WorldGameTests {
|
||||||
|
private record Witness(ExpansionIsland island, BlockPos deep, BlockPos shore, BlockPos exterior) {}
|
||||||
|
|
||||||
|
@GameTest(maxTicks = 18000)
|
||||||
|
public void oceanSizesClimatesAndNativeIce(GameTestHelper helper) throws Exception {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 23, "Ocean matrix requires a disposable generation23 world");
|
||||||
|
var prior = generator.source().islands();
|
||||||
|
var directory = level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory());
|
||||||
|
var journalBefore = files(directory);
|
||||||
|
var witnesses = new ArrayList<Witness>();
|
||||||
|
int index = 0;
|
||||||
|
for (int diameter : new int[]{512, 1024}) for (String climate : List.of("glacial", "temperate", "warm")) {
|
||||||
|
var island = new ExpansionIsland("ocean_" + diameter + "_" + climate, "sanctuary", "east", climate,
|
||||||
|
diameter, 8192 + index * 4096, 8192, 23042L + index * 101L, "ocean", "ready", 23);
|
||||||
|
witnesses.add(witness(island)); index++;
|
||||||
|
}
|
||||||
|
var published = new ArrayList<>(prior); witnesses.forEach(w -> published.add(w.island()));
|
||||||
|
var chunks = new TreeMap<Long, ChunkPos>();
|
||||||
|
for (var w : witnesses) {
|
||||||
|
for (var position : List.of(w.deep(), w.shore(), w.exterior())) {
|
||||||
|
var chunk = ChunkPos.containing(position); chunks.put(chunk.pack(), chunk);
|
||||||
|
}
|
||||||
|
// Native icebergs can write eleven blocks to either side of their origin.
|
||||||
|
if (w.island().climate().equals("glacial")) ChunkPos.rangeClosed(ChunkPos.containing(w.deep()), 1)
|
||||||
|
.forEach(chunk -> chunks.put(chunk.pack(), chunk));
|
||||||
|
}
|
||||||
|
var iterator = chunks.values().iterator();
|
||||||
|
var report = new LinkedHashMap<String, Object>();
|
||||||
|
generator.setIslands(published);
|
||||||
|
Runnable[] step = new Runnable[1];
|
||||||
|
step[0] = () -> {
|
||||||
|
try {
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
var chunk = iterator.next(); report.put("stage", "full " + chunk);
|
||||||
|
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||||
|
// Reusing the executing Runnable would discard its next scheduled step.
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||||
|
}
|
||||||
|
var profiles = new ArrayList<Map<String, Object>>();
|
||||||
|
report.put("profiles", profiles);
|
||||||
|
for (var w : witnesses) {
|
||||||
|
report.put("stage", "inspect " + w.island().id()); profiles.add(inspect(helper, generator, w));
|
||||||
|
}
|
||||||
|
report.put("stage", "restore and verify journal");
|
||||||
|
helper.assertTrue(ExpansionOcean23.decorationSeaLevel() == null && NativeStructurePlacement23.seaLevel() == null,
|
||||||
|
"All ocean and structure scopes are restored after native decoration");
|
||||||
|
helper.assertTrue(generator.getSeaLevel() == -64, "Local oceans do not replace the global void sea level");
|
||||||
|
generator.setIslands(prior);
|
||||||
|
helper.assertTrue(generator.source().islands().equals(prior), "The original activation snapshot is restored");
|
||||||
|
helper.assertTrue(equalFiles(journalBefore, files(directory)), "Temporary test islands never alter the expansion journal or certificates");
|
||||||
|
report.put("generation", 23); report.put("world_seed", level.getSeed()); report.put("profiles", profiles);
|
||||||
|
report.put("full_chunks_requested", chunks.size()); report.put("process_id", ProcessHandle.current().pid());
|
||||||
|
report.put("journal_unchanged", true); report.put("passed", true);
|
||||||
|
Path output = Path.of("diagnostics", "ocean23"); Files.createDirectories(output);
|
||||||
|
Files.writeString(output.resolve("full.json"), new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
|
helper.succeed();
|
||||||
|
} catch (Exception error) {
|
||||||
|
generator.setIslands(prior);
|
||||||
|
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());
|
||||||
|
fr.koka.sanctuary.SanctuaryMod.LOGGER.error("Ocean23 matrix failed at {}", report.get("stage"), error);
|
||||||
|
try {
|
||||||
|
Path output = Path.of("diagnostics", "ocean23"); Files.createDirectories(output);
|
||||||
|
Files.writeString(output.resolve("failure.json"), new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
|
} catch (java.io.IOException writeError) { error.addSuppressed(writeError); }
|
||||||
|
throw new IllegalStateException("Ocean23 matrix failed: " + error.getMessage(), error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Witness witness(ExpansionIsland island) {
|
||||||
|
var deep = new BlockPos(island.centerX() + 8, ExpansionOcean23.seaLevel(island) - 8, island.centerZ() + 8);
|
||||||
|
BlockPos shore = null;
|
||||||
|
// Quart-aligned samples exercise the stored biome palette without biome-boundary jitter.
|
||||||
|
for (int z = island.centerZ() - island.diameter() / 2; z <= island.centerZ() + island.diameter() / 2 && shore == null; z += 4)
|
||||||
|
for (int x = island.centerX() - island.diameter() / 2; x <= island.centerX() + island.diameter() / 2; x += 4) {
|
||||||
|
var c = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (!c.water() && c.influence() == 1 && c.floorY() <= c.waterLevel() + 8) {
|
||||||
|
shore = new BlockPos(x, (c.floorY() + 4) & ~3, z); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shore == null) throw new IllegalStateException("Ocean profile lacks a dry shoreline witness");
|
||||||
|
var exterior = new BlockPos(island.centerX() + island.terrainRadius() + 96, deep.getY(), island.centerZ());
|
||||||
|
return new Witness(island, deep, shore, exterior);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> inspect(GameTestHelper helper, SanctuaryChunkGenerator generator, Witness w) {
|
||||||
|
var level = helper.getLevel(); var island = w.island();
|
||||||
|
var c = ExpansionOcean23.column(island, w.deep().getX(), w.deep().getZ());
|
||||||
|
helper.assertTrue(c.deep(), "Matrix sample lies in the deep ocean: " + island.id());
|
||||||
|
var deepChunk = (LevelChunk) level.getChunk(w.deep().getX() >> 4, w.deep().getZ() >> 4, ChunkStatus.FULL, true);
|
||||||
|
int solid = 0, submerged = 0;
|
||||||
|
for (int z = deepChunk.getPos().getMinBlockZ(); z <= deepChunk.getPos().getMaxBlockZ(); z += 4)
|
||||||
|
for (int x = deepChunk.getPos().getMinBlockX(); x <= deepChunk.getPos().getMaxBlockX(); x += 4) {
|
||||||
|
var column = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (deepChunk.getBlockState(new BlockPos(x, column.floorY() - 3, z)).isSolid()) solid++;
|
||||||
|
var state = deepChunk.getBlockState(new BlockPos(x, column.waterLevel() - 3, z));
|
||||||
|
if (state.getFluidState().is(FluidTags.WATER) || state.is(Blocks.ICE) || state.is(Blocks.PACKED_ICE) || state.is(Blocks.BLUE_ICE)) submerged++;
|
||||||
|
}
|
||||||
|
helper.assertTrue(solid >= 12, "Native features retain a supported seabed: " + island.id() + " " + solid + "/16");
|
||||||
|
helper.assertTrue(submerged >= 12, "The deep basin remains water or natural ice after FEATURES: " + island.id() + " " + submerged + "/16");
|
||||||
|
String deepBiome = biome(level, w.deep());
|
||||||
|
helper.assertTrue(deepBiome.equals("minecraft:" + ExpansionOcean23.biome(island, c)), "Stored deep-ocean biome matches climate: " + island.id());
|
||||||
|
var shoreColumn = ExpansionOcean23.column(island, w.shore().getX(), w.shore().getZ());
|
||||||
|
helper.assertTrue(level.getBlockState(w.shore().atY(shoreColumn.floorY() - 8)).isSolid(), "Dry shore has a real foundation: " + island.id());
|
||||||
|
String shoreBiome = biome(level, w.shore());
|
||||||
|
helper.assertTrue(shoreBiome.equals("minecraft:" + ExpansionOcean23.shoreBiome(island)), "Stored shoreline biome matches its profile: " + island.id());
|
||||||
|
var outside = level.getChunk(w.exterior().getX() >> 4, w.exterior().getZ() >> 4, ChunkStatus.FULL, true);
|
||||||
|
for (int y = level.getMinY(); y <= level.getMaxY(); y++)
|
||||||
|
helper.assertTrue(outside.getBlockState(w.exterior().atY(y)).isAir(), "Ocean leaves exterior void empty: " + island.id() + " y=" + y);
|
||||||
|
helper.assertTrue(biome(level, w.exterior()).equals("minecraft:the_void"), "Exterior biome remains void");
|
||||||
|
var result = new LinkedHashMap<String, Object>();
|
||||||
|
result.put("island", island); result.put("deep", w.deep().toShortString()); result.put("shore", w.shore().toShortString());
|
||||||
|
result.put("water_level", c.waterLevel()); result.put("floor", c.floorY()); result.put("deep_biome", deepBiome);
|
||||||
|
result.put("shore_biome", shoreBiome); result.put("solid_samples", solid); result.put("submerged_samples", submerged);
|
||||||
|
if (island.climate().equals("glacial")) result.put("forced_native_ice_witness", ice(helper, generator, w));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> ice(GameTestHelper helper, SanctuaryChunkGenerator generator, Witness witness) {
|
||||||
|
var level = helper.getLevel(); var origin = witness.deep(); var centre = ChunkPos.containing(origin);
|
||||||
|
int sea = ExpansionOcean23.seaLevel(witness.island());
|
||||||
|
var holders = StaticCache2D.<GenerationChunkHolder>create(centre.x(), centre.z(), 1, (x, z) -> {
|
||||||
|
var full = (LevelChunk) level.getChunk(x, z, ChunkStatus.FULL, true);
|
||||||
|
var writable = new ImposterProtoChunk(full, true);
|
||||||
|
return new GenerationChunkHolder(full.getPos()) {
|
||||||
|
@Override protected void addSaveDependency(java.util.concurrent.CompletableFuture<?> dependency) {}
|
||||||
|
@Override public int getTicketLevel() { return 0; }
|
||||||
|
@Override public int getQueueLevel() { return 0; }
|
||||||
|
@Override public ChunkAccess getChunkIfPresentUnchecked(ChunkStatus status) { return writable; }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var centreChunk = level.getChunk(centre.x(), centre.z(), ChunkStatus.FULL, true);
|
||||||
|
var probe = new ScopeProbe(level, holders, centreChunk);
|
||||||
|
probe.probing = true;
|
||||||
|
boolean observed = false;
|
||||||
|
try { generator.applyBiomeDecoration(probe, centreChunk, level.structureManager()); }
|
||||||
|
catch (ScopeObserved stop) { observed = true; }
|
||||||
|
helper.assertTrue(observed && probe.generatorSea == sea && probe.regionSea == sea && !probe.nativePlacement,
|
||||||
|
"Production FEATURES scope sets both sea levels without bypassing water retention");
|
||||||
|
helper.assertTrue(ExpansionOcean23.decorationSeaLevel() == null && generator.getSeaLevel() == -64,
|
||||||
|
"Production FEATURES scope restores its level even when decoration fails");
|
||||||
|
|
||||||
|
var region = new WorldGenRegion(level, holders, ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES), centreChunk);
|
||||||
|
Integer previous = ExpansionOcean23.enterDecoration(sea);
|
||||||
|
int iceAbove = 0, highest = Integer.MIN_VALUE;
|
||||||
|
try {
|
||||||
|
// This native draw has height 9 / width 11. Seed 23042 instead yields
|
||||||
|
// height 3 / width 2: vanilla may legitimately return true without ice.
|
||||||
|
helper.assertTrue(new IcebergFeature(Blocks.PACKED_ICE.defaultBlockState()).place(region, generator,
|
||||||
|
RandomSource.create(23041L), origin.atY(0)), "Native iceberg feature runs in the local ocean scope");
|
||||||
|
for (int x = origin.getX() - 11; x <= origin.getX() + 11; x++)
|
||||||
|
for (int z = origin.getZ() - 11; z <= origin.getZ() + 11; z++) for (int y = sea + 1; y <= sea + 48; y++) {
|
||||||
|
var state = region.getBlockState(new BlockPos(x, y, z));
|
||||||
|
if (state.is(Blocks.PACKED_ICE) || state.is(Blocks.BLUE_ICE)) { iceAbove++; highest = Math.max(highest, y); }
|
||||||
|
}
|
||||||
|
helper.assertTrue(iceAbove > 10 && highest > sea,
|
||||||
|
"Native iceberg forms above the floating ocean: ice=" + iceAbove + ", highest=" + highest + ", sea=" + sea);
|
||||||
|
var blue = origin.offset(5, 0, 5).atY(sea - 4);
|
||||||
|
region.setBlock(blue, Blocks.WATER.defaultBlockState(), 2, 0);
|
||||||
|
region.setBlock(blue.east(), Blocks.PACKED_ICE.defaultBlockState(), 2, 0);
|
||||||
|
helper.assertTrue(new BlueIceFeature().place(region, generator, RandomSource.create(23043L), blue)
|
||||||
|
&& region.getBlockState(blue).is(Blocks.BLUE_ICE), "Native blue ice accepts the same local sea level");
|
||||||
|
var floor = ExpansionOcean23.column(witness.island(), origin.getX(), origin.getZ());
|
||||||
|
var retained = origin.atY(floor.floorY() - 3);
|
||||||
|
var before = region.getBlockState(retained);
|
||||||
|
helper.assertTrue(!region.setBlock(retained, Blocks.AIR.defaultBlockState(), 2, 0)
|
||||||
|
&& region.getBlockState(retained).equals(before), "Decoration sea-level scope cannot punch through the retaining shell");
|
||||||
|
} finally { ExpansionOcean23.restoreDecoration(previous); }
|
||||||
|
helper.assertTrue(NativeStructurePlacement23.seaLevel() == null && ExpansionOcean23.decorationSeaLevel() == null,
|
||||||
|
"Native ice witnesses leave no thread-local state");
|
||||||
|
return Map.of("iceberg_feature_seed", 23041L, "ice_above_sea", iceAbove, "highest_ice", highest, "sea_level", sea,
|
||||||
|
"blue_ice", true, "production_scope_checked", true, "retention_preserved", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Abort at the first random-seed read, before replaying any structures or features. */
|
||||||
|
private static final class ScopeProbe extends WorldGenRegion {
|
||||||
|
boolean probing, nativePlacement; int generatorSea, regionSea;
|
||||||
|
ScopeProbe(ServerLevel level, StaticCache2D<GenerationChunkHolder> holders, ChunkAccess centre) {
|
||||||
|
super(level, holders, ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES), centre);
|
||||||
|
}
|
||||||
|
@Override public long getSeed() {
|
||||||
|
if (!probing) return super.getSeed();
|
||||||
|
generatorSea = getLevel().getChunkSource().getGenerator().getSeaLevel(); regionSea = getSeaLevel();
|
||||||
|
nativePlacement = NativeStructurePlacement23.seaLevel() != null;
|
||||||
|
throw new ScopeObserved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static final class ScopeObserved extends RuntimeException {}
|
||||||
|
private static String biome(ServerLevel level, BlockPos pos) {
|
||||||
|
return level.getNoiseBiome(pos.getX() >> 2, pos.getY() >> 2, pos.getZ() >> 2).unwrapKey().orElseThrow().identifier().toString();
|
||||||
|
}
|
||||||
|
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
|
||||||
|
var files = new TreeMap<String, byte[]>();
|
||||||
|
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
|
||||||
|
for (var path : paths.filter(Files::isRegularFile).toList()) files.put(directory.relativize(path).toString(), Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
private static boolean equalFiles(Map<String, byte[]> before, Map<String, byte[]> after) {
|
||||||
|
return before.keySet().equals(after.keySet()) && before.entrySet().stream().allMatch(e -> Arrays.equals(e.getValue(), after.get(e.getKey())));
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -58,11 +58,15 @@ public final class PopulationDiagnostics {
|
|||||||
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||||
"New cave decorations must refuse older saved worlds");
|
"New cave decorations must refuse older saved worlds");
|
||||||
}
|
}
|
||||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator) {
|
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) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
|
||||||
|
.forEach(name -> expected.add("minecraft:" + name));
|
||||||
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
|
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
|
||||||
&& generator.getBiomeSource().possibleBiomes().size() == (((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().citiesEnabled() ? 45 : 44)
|
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
|
||||||
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName()).collect(java.util.stream.Collectors.toSet()).containsAll(SanctuaryBiomeSource.REQUIRED_BIOMES),
|
.collect(java.util.stream.Collectors.toSet()).equals(expected),
|
||||||
"The unified generator retains all 44 historical biome holders, with Lost City added in city-enabled generations");
|
"Historical holders stay intact; only generation23 adds the expansion ocean and shore palette");
|
||||||
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
|
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
|
||||||
"The unified starter must use its real terrain state, never the expansion host fallback");
|
"The unified starter must use its real terrain state, never the expansion host fallback");
|
||||||
} else {
|
} else {
|
||||||
@@ -211,7 +215,7 @@ public final class PopulationDiagnostics {
|
|||||||
|
|
||||||
void prepare() throws IOException {
|
void prepare() throws IOException {
|
||||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||||
&& (sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
|
&& (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.
|
// 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.
|
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||||
// Historical generators retain their original near-spawn contract.
|
// Historical generators retain their original near-spawn contract.
|
||||||
|
|||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
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.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.transit.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.mineral.*;
|
||||||
|
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.Registries;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** Read-only admission audit, then native starts and reload in the disposable development world. */
|
||||||
|
public final class Secrets23WorldGameTests {
|
||||||
|
@GameTest(maxTicks = 12000)
|
||||||
|
public void naturalSecretsPersist(GameTestHelper helper) throws Exception {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 23, "Use a fresh23 development world");
|
||||||
|
if (!level.structureManager().shouldGenerateStructures()) { verifyGlobalOff(helper, generator); return; }
|
||||||
|
var root = ExpansionIsland.origin(generator.initialDiameter(), level.getSeed(), 23);
|
||||||
|
var heritage = generator.heritagePlan19();
|
||||||
|
var transit = generator.transitPlan21();
|
||||||
|
var mineral = generator.mineralSanctuary23();
|
||||||
|
var ruins = generator.ruinsPlan17();
|
||||||
|
helper.assertTrue(generator.lostCityPlan(root).isEmpty(), "No surface city on the natural starter island");
|
||||||
|
helper.assertTrue(heritage.sites().stream().allMatch(s -> s.kind() == HeritagePlan19.Kind.OBSERVATORY), "Only isolated observatory is selected");
|
||||||
|
helper.assertTrue(ruins.networks().size() <= 1, "At most one hidden workshop network");
|
||||||
|
for (var network : ruins.networks()) {
|
||||||
|
helper.assertTrue(network.rooms().stream().anyMatch(r -> r.kind() == RuinsPlan17.Kind.WORKSHOP), "Secret network contains its workshop");
|
||||||
|
helper.assertTrue(network.rooms().stream().allMatch(r -> r.kind() == RuinsPlan17.Kind.ACCESS
|
||||||
|
|| r.kind() == RuinsPlan17.Kind.CORRIDOR || r.kind() == RuinsPlan17.Kind.WORKSHOP), "No unselected rooms");
|
||||||
|
}
|
||||||
|
for (var segment : heritage.segments()) for (var cell : segment.cells())
|
||||||
|
helper.assertTrue(Math.hypot(cell.x(), cell.z()) >= 48, "Surface routes preserve natural arrival");
|
||||||
|
if (generator.experimentalStructures()) helper.assertTrue(heritage.segments().stream()
|
||||||
|
.anyMatch(s -> s.kind() == HeritagePlan19.RouteKind.FREIGHT && s.cells().stream().anyMatch(c -> c.railPresent())),
|
||||||
|
"Reference island retains a real repairable rail run, not only endpoint markers");
|
||||||
|
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
|
||||||
|
var terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||||
|
int columns = 0;
|
||||||
|
// The three samplers have independent bounded caches, just like production planning.
|
||||||
|
terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||||
|
for (var site : heritage.sites()) {
|
||||||
|
helper.assertTrue(site.supports().isEmpty(), "No heritage rescue piers");
|
||||||
|
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
|
||||||
|
helper.assertTrue(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Building cut/fill stays within two blocks");
|
||||||
|
helper.assertTrue(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Every foundation column rests on original rock"); columns++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.assertTrue(!transit.isEmpty(), "The reference island retains its core traversal");
|
||||||
|
var hall = generator.undergroundHall22();
|
||||||
|
helper.assertTrue(!hall.isEmpty(), "Core hall exists independently of a surface town");
|
||||||
|
helper.assertTrue(hall.networks().stream().flatMap(n -> n.modules().stream()).noneMatch(m -> m.kind() == LostCityUnderground16.Kind.POOLROOM), "Pool rooms stay outside the active selection");
|
||||||
|
for (var module : transit.modules()) for (var support : module.supports())
|
||||||
|
helper.assertTrue(support.maxY() - support.minY() + 1 <= 16, "Core traversal retains bounded engineered supports");
|
||||||
|
if (!generator.experimentalStructures()) helper.assertTrue(heritage.sites().isEmpty() && heritage.segments().isEmpty() && ruins.isEmpty() && mineral.isEmpty(), "OFF removes optional catalogue, not core hall and traversal");
|
||||||
|
var gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
var encoded = ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
|
||||||
|
var geometry = gson.toJsonTree(Map.of("mineral", mineral.isEmpty() ? List.of() : List.of(mineral.chamber()), "ruins", ruins.networks(), "heritage_sites", heritage.sites(),
|
||||||
|
"heritage_links", heritage.links(), "heritage_segments", heritage.segments(), "transit", transit.modules(), "independent_hall", hall.networks()));
|
||||||
|
var report = new LinkedHashMap<String, Object>();
|
||||||
|
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||||
|
report.put("process_id", ProcessHandle.current().pid()); report.put("generator_codec", encoded); report.put("geometry", geometry);
|
||||||
|
report.put("experimental", generator.experimentalStructures()); report.put("city_districts", 0); report.put("mineral_accepted", !mineral.isEmpty()); report.put("ruins_networks", ruins.networks().size());
|
||||||
|
report.put("heritage_sites", heritage.sites().size()); report.put("heritage_links", heritage.links().size()); report.put("transit_modules", transit.modules().size());
|
||||||
|
report.put("independent_hall_networks", hall.networks().size());
|
||||||
|
report.put("core_entries", transit.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).map(m -> List.of(m.centerX(), m.surfaceY() + 1, m.minZ() - 2)).toList());
|
||||||
|
report.put("hall_entries", hall.networks().stream().map(n -> { var at = LostCityUndergroundStructures22.locator(n); return List.of(at.getX(), at.getY(), at.getZ()); }).toList());
|
||||||
|
report.put("natural_columns_audited", columns);
|
||||||
|
report.put("mineral_diagnostics", mineral.diagnostic());
|
||||||
|
report.put("heritage_diagnostics", heritage.diagnostics()); report.put("transit_diagnostics", transit.diagnostic());
|
||||||
|
var starts = new ArrayList<StructureStart>();
|
||||||
|
var origins = new LinkedHashMap<ResourceKey<Structure>, Set<ChunkPos>>();
|
||||||
|
origins.put(LostCityStructures.KEY, new HashSet<>()); origins.put(HeritageStructures19.KEY, new HashSet<>()); origins.put(TransitStructures21.KEY, new HashSet<>()); origins.put(LostCityUndergroundStructures22.KEY, new HashSet<>());
|
||||||
|
origins.put(RuinsStructures17.KEY, new HashSet<>()); origins.put(MineralSanctuaryStructures23.KEY, new HashSet<>());
|
||||||
|
ruins.networks().forEach(n -> origins.get(RuinsStructures17.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||||
|
if (!mineral.isEmpty()) origins.get(MineralSanctuaryStructures23.KEY).add(new ChunkPos(mineral.chamber().startChunkX(), mineral.chamber().startChunkZ()));
|
||||||
|
heritage.sites().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
heritage.segments().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
transit.modules().forEach(m -> origins.get(TransitStructures21.KEY).add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
|
||||||
|
hall.networks().forEach(n -> origins.get(LostCityUndergroundStructures22.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||||
|
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||||
|
var nativeTags = new TreeMap<String, String>();
|
||||||
|
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||||
|
for (var entry : origins.entrySet()) for (var origin : entry.getValue()) {
|
||||||
|
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(entry.getKey()).value();
|
||||||
|
var start = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(type);
|
||||||
|
helper.assertTrue(start != null && start.isValid(), "Accepted geometry must have a native start");
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
helper.assertTrue(!(piece instanceof LostCityFoundationPiece16), "No artificial city foundation emitted");
|
||||||
|
if (entry.getKey().equals(HeritageStructures19.KEY)) helper.assertTrue(piece instanceof HeritagePiece21, "Use published21 architecture");
|
||||||
|
if (entry.getKey().equals(LostCityUndergroundStructures22.KEY)) helper.assertTrue(piece instanceof LostCityUndergroundPiece20, "Independent hall keeps published20 room geometry");
|
||||||
|
if (entry.getKey().equals(TransitStructures21.KEY)) helper.assertTrue(piece instanceof TransitPiece21 || piece instanceof TransitPiece22, "Published21 tunnel geometry with sealed22 corners");
|
||||||
|
if (entry.getKey().equals(MineralSanctuaryStructures23.KEY)) helper.assertTrue(piece instanceof MineralSanctuaryPiece23, "Saved mineral23 geometry");
|
||||||
|
if (entry.getKey().equals(RuinsStructures17.KEY)) helper.assertTrue(piece instanceof RuinsPiece20, "Saved workshop20 geometry");
|
||||||
|
piece.getBoundingBox().intersectingChunks().forEach(chunks::add);
|
||||||
|
}
|
||||||
|
var tag = start.createTag(context, origin);
|
||||||
|
var loaded = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||||
|
helper.assertTrue(loaded != null && tag.equals(loaded.createTag(context, origin)), "Native start round trip");
|
||||||
|
nativeTags.put(entry.getKey().identifier() + "/" + origin.x() + "/" + origin.z(), tag.toString()); starts.add(start);
|
||||||
|
}
|
||||||
|
if (chunks.isEmpty()) chunks.add(new ChunkPos(0, 0));
|
||||||
|
report.put("native_starts", nativeTags); report.put("full_chunks", chunks.size());
|
||||||
|
Path world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||||
|
Path proof = world.resolve("secrets23-create.json");
|
||||||
|
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||||
|
JsonObject previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
|
||||||
|
if (reload) {
|
||||||
|
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload follows a successful separate JVM");
|
||||||
|
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("geometry").equals(geometry), "Generation, options and accepted/refused placements survive reload");
|
||||||
|
helper.assertTrue(previous.get("native_starts").equals(gson.toJsonTree(nativeTags)), "Native saved starts survive reload");
|
||||||
|
}
|
||||||
|
var iterator = chunks.iterator();
|
||||||
|
Runnable finish = () -> {
|
||||||
|
try {
|
||||||
|
// Refresh already-generated transit chunks: the earlier short FULL tickets can expire
|
||||||
|
// while the catalogue loads. The walk checks real native collision shapes, including turns.
|
||||||
|
var transitChunks = new HashSet<ChunkPos>();
|
||||||
|
transit.modules().forEach(m -> TransitPiece21.bounds(m).intersectingChunks().forEach(transitChunks::add));
|
||||||
|
for (var pos : transitChunks) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||||
|
int nativeRails = 0;
|
||||||
|
for (var segment : heritage.segments()) {
|
||||||
|
level.getChunk(segment.startChunkX(), segment.startChunkZ(), ChunkStatus.FULL, true);
|
||||||
|
for (var cell : segment.cells()) if (cell.railPresent()) {
|
||||||
|
var at = new BlockPos(cell.x(), cell.floorY() + 1, cell.z());
|
||||||
|
helper.assertTrue(level.getBlockState(at).is(Blocks.RAIL), "Planned rail survives in actual FULL chunk");
|
||||||
|
helper.assertTrue(level.getBlockState(at.below()).isSolid(), "Native rail remains supported by its ballast");
|
||||||
|
nativeRails++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (generator.experimentalStructures()) helper.assertTrue(nativeRails > 0, "Real repairable railway is present after chunk decoration");
|
||||||
|
report.put("native_rail_blocks", nativeRails);
|
||||||
|
report.put("transit_native_walk", Transit21WorldGameTests.verifyAccess(helper, transit));
|
||||||
|
report.put("hall_native_access", Underground22NativeChecks.verify(helper, hall.isEmpty() ? generator.undergroundPlan16(root) : hall));
|
||||||
|
if (!mineral.isEmpty()) {
|
||||||
|
var c = mineral.chamber();
|
||||||
|
for (var pos : c.bounds().intersectingChunks().toList()) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||||
|
for (int dz = -19; dz <= 19; dz += 3) for (int dx = -19; dx <= 19; dx += 3) {
|
||||||
|
var floor = new BlockPos(c.centerX() + dx, c.minY() + 2, c.centerZ() + dz);
|
||||||
|
helper.assertTrue(level.getBlockState(floor).isSolid(), "Mineral native central floor remains walkable");
|
||||||
|
helper.assertTrue(level.getBlockState(floor.above()).isAir() && level.getBlockState(floor.above(2)).isAir(), "Mineral native central room remains clear");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!heritage.sites().isEmpty()) {
|
||||||
|
var site = heritage.sites().getFirst();
|
||||||
|
var witness = new BlockPos(site.centerX(), site.floorY() - 1, site.centerZ());
|
||||||
|
if (reload) helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK), "Personal edit in disposable test survives reload");
|
||||||
|
else {
|
||||||
|
helper.assertTrue(level.getBlockState(witness).isSolid(), "Accepted building foundation generated in real FULL chunks");
|
||||||
|
level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||||
|
}
|
||||||
|
report.put("saved_edit", List.of(witness.getX(), witness.getY(), witness.getZ()));
|
||||||
|
}
|
||||||
|
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native world before shutdown");
|
||||||
|
report.put("passed", true);
|
||||||
|
var directory = Path.of("diagnostics", "secrets23-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
|
||||||
|
var text = gson.toJson(report) + "\n"; Files.writeString(directory.resolve(reload ? "reload.json" : "create.json"), text);
|
||||||
|
if (!reload) Files.writeString(proof, text);
|
||||||
|
helper.succeed();
|
||||||
|
} catch (Exception failure) { throw new IllegalStateException(failure); }
|
||||||
|
};
|
||||||
|
Runnable[] load = new Runnable[1];
|
||||||
|
load[0] = () -> {
|
||||||
|
if (!iterator.hasNext()) { finish.run(); return; }
|
||||||
|
var pos = iterator.next(); level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||||
|
helper.runAtTickTime(helper.getTick() + 1, () -> load[0].run());
|
||||||
|
};
|
||||||
|
load[0].run();
|
||||||
|
}
|
||||||
|
private static void verifyGlobalOff(GameTestHelper helper, SanctuaryChunkGenerator generator) throws Exception {
|
||||||
|
var level = helper.getLevel();
|
||||||
|
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||||
|
chunks.add(new ChunkPos(0, 0)); chunks.add(new ChunkPos(4, 4)); chunks.add(new ChunkPos(-4, -4));
|
||||||
|
generator.heritagePlan19().sites().forEach(s -> chunks.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
generator.heritagePlan19().segments().forEach(s -> chunks.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||||
|
generator.ruinsPlan17().networks().forEach(n -> chunks.add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||||
|
generator.transitPlan21().modules().forEach(m -> chunks.add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
|
||||||
|
generator.undergroundHall22().networks().forEach(n -> chunks.add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||||
|
var mineral = generator.mineralSanctuary23();
|
||||||
|
if (!mineral.isEmpty()) chunks.add(new ChunkPos(mineral.chamber().startChunkX(), mineral.chamber().startChunkZ()));
|
||||||
|
var manufactured = Set.of(Blocks.RAIL, Blocks.STONE_BRICKS, Blocks.STONE_BRICK_STAIRS, Blocks.CHISELED_STONE_BRICKS,
|
||||||
|
Blocks.IRON_BARS, Blocks.SMOOTH_STONE);
|
||||||
|
int checked = 0;
|
||||||
|
for (var pos : chunks) {
|
||||||
|
var chunk = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||||
|
helper.assertTrue(chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid), "GlobalOFF emits no native structure start");
|
||||||
|
var at = new BlockPos.MutableBlockPos();
|
||||||
|
for (int z = pos.getMinBlockZ(); z <= pos.getMaxBlockZ(); z++) for (int x = pos.getMinBlockX(); x <= pos.getMaxBlockX(); x++)
|
||||||
|
for (int y = 1; y < 383; y++) {
|
||||||
|
at.set(x, y, z);
|
||||||
|
helper.assertTrue(!manufactured.contains(chunk.getBlockState(at).getBlock()), "GlobalOFF leaves no generated secret architecture or rails");
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var directory = Path.of("diagnostics", "secrets23-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
|
||||||
|
Files.writeString(directory.resolve("global-off.json"), new GsonBuilder().setPrettyPrinting().create().toJson(
|
||||||
|
Map.of("passed", true, "structures_enabled", false, "chunks", chunks.size(), "blocks_checked", checked)) + "\n");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+8
-5
@@ -58,7 +58,10 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
var historical21 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v21")));
|
var historical21 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v21")));
|
||||||
helper.assertTrue(historical21.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 21
|
helper.assertTrue(historical21.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 21
|
||||||
&& old.source().generation() == 21 && old.experimentalStructures(), "Historical21 keeps its original placement rules");
|
&& old.source().generation() == 21 && old.experimentalStructures(), "Historical21 keeps its original placement rules");
|
||||||
helper.assertTrue(generator.rootGeneration() == 22 && generator.source().generation() == 22 && generator.experimentalStructures(), "The public preset uses generation22 with its new-world option");
|
var historical22 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v22")));
|
||||||
|
helper.assertTrue(historical22.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
|
||||||
|
&& old.rootGeneration() == 22 && old.source().generation() == 22, "Historical22 remains separately readable");
|
||||||
|
helper.assertTrue(generator.rootGeneration() == 23 && generator.source().generation() == 23 && generator.experimentalStructures(), "The public preset uses generation23 with its new-world option");
|
||||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||||
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
|
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
|
||||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||||
@@ -69,11 +72,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
var activeCodec = 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() == 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();
|
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||||
serialized.remove("experimental_structures");
|
serialized.remove("experimental_structures");
|
||||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||||
"Generation22 omitted experimental option defaults to enabled");
|
"Generation23 omitted experimental option defaults to enabled");
|
||||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||||
serialized.remove("initial_diameter");
|
serialized.remove("initial_diameter");
|
||||||
@@ -132,7 +135,7 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
}
|
}
|
||||||
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
|
// Reuse the attached world's real plans: no extra native-noise planning or chunk loads.
|
||||||
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|
if (!(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator active)
|
||||||
|| active.rootGeneration() != 21 && active.rootGeneration() != 22) return;
|
|| active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23) return;
|
||||||
helper.assertTrue(!active.protectsCityDecoration(outside)
|
helper.assertTrue(!active.protectsCityDecoration(outside)
|
||||||
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
||||||
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
|
outside.getX() + 1, outside.getY() + 1, outside.getZ() + 1),
|
||||||
@@ -142,7 +145,7 @@ final class UnifiedWorldSettingsDiagnostics {
|
|||||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||||
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
|
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
|
||||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||||
if (active.rootGeneration() == 22 && helper.getLevel().structureManager().shouldGenerateStructures())
|
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23) && helper.getLevel().structureManager().shouldGenerateStructures())
|
||||||
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
|
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect");
|
||||||
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
|
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
|
||||||
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
|
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
|
||||||
|
|||||||
+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.
|
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
||||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||||
if (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.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.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")) {
|
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||||
long until = System.nanoTime() + 20_000_000L;
|
long until = System.nanoTime() + 20_000_000L;
|
||||||
@@ -40,16 +40,16 @@ public abstract class SanctuaryGameTestServerMixin {
|
|||||||
@ModifyArg(method = "<clinit>", at = @At(value = "INVOKE", target =
|
@ModifyArg(method = "<clinit>", at = @At(value = "INVOKE", target =
|
||||||
"Lnet/minecraft/world/level/levelgen/WorldOptions;<init>(JZZ)V"), index = 1)
|
"Lnet/minecraft/world/level/levelgen/WorldOptions;<init>(JZZ)V"), index = 1)
|
||||||
private static boolean sanctuary$enableStructureGeneration(boolean original) {
|
private static boolean sanctuary$enableStructureGeneration(boolean original) {
|
||||||
return true;
|
return Boolean.parseBoolean(System.getProperty("sanctuary.test.generateStructures", "true"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
||||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||||
int generation = Integer.getInteger("sanctuary.test.generation", 22);
|
int generation = Integer.getInteger("sanctuary.test.generation", 23);
|
||||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) throw new IllegalArgumentException("Unsupported test generation");
|
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) throw new IllegalArgumentException("Unsupported test generation");
|
||||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 22 ? "sanctuary" : "sanctuary_v" + generation));
|
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 23 ? "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.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public final class SanctuaryMod implements ModInitializer {
|
|||||||
@Override
|
@Override
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
fr.koka.sanctuary.knowledge.census.WorldCensusService.register();
|
fr.koka.sanctuary.knowledge.census.WorldCensusService.register();
|
||||||
|
fr.koka.sanctuary.knowledge.structures.StructureCensusService.register();
|
||||||
fr.koka.sanctuary.knowledge.history.MaterialActivityHistory.register();
|
fr.koka.sanctuary.knowledge.history.MaterialActivityHistory.register();
|
||||||
fr.koka.sanctuary.knowledge.BlockKnowledgeService.register();
|
fr.koka.sanctuary.knowledge.BlockKnowledgeService.register();
|
||||||
fr.koka.sanctuary.knowledge.BlockKnowledgeNetworking.register();
|
fr.koka.sanctuary.knowledge.BlockKnowledgeNetworking.register();
|
||||||
@@ -73,6 +74,9 @@ public final class SanctuaryMod implements ModInitializer {
|
|||||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v21"), SanctuaryBiomeSource.NETWORK21_CODEC);
|
||||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v22"), SanctuaryChunkGenerator.SELECTIVE22_CODEC);
|
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v22"), SanctuaryChunkGenerator.SELECTIVE22_CODEC);
|
||||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v22"), SanctuaryBiomeSource.SELECTIVE22_CODEC);
|
||||||
|
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v23"), SanctuaryChunkGenerator.SECRETS23_CODEC);
|
||||||
|
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC);
|
||||||
|
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
|
||||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
|
||||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
||||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.register();
|
||||||
|
|||||||
+2
-2
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
|||||||
contents.addChild(sizeButton);
|
contents.addChild(sizeButton);
|
||||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||||
.setMaxWidth(310).setCentered(true));
|
.setMaxWidth(310).setCentered(true));
|
||||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) {
|
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) {
|
||||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
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"),
|
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||||
(button, value) -> experimental = value));
|
(button, value) -> experimental = value));
|
||||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
contents.addChild(new MultiLineTextWidget(Component.translatable(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) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
|
||||||
.setMaxWidth(310).setCentered(true));
|
.setMaxWidth(310).setCentered(true));
|
||||||
}
|
}
|
||||||
layout.addToContents(contents);
|
layout.addToContents(contents);
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ public class ExpansionBiomeSource extends BiomeSource {
|
|||||||
protected Holder<Biome> resolve(List<ExpansionIsland> snapshot, int x, int y, int z) {
|
protected Holder<Biome> resolve(List<ExpansionIsland> snapshot, int x, int y, int z) {
|
||||||
ExpansionIsland island = islandAt(snapshot, x, z);
|
ExpansionIsland island = islandAt(snapshot, x, z);
|
||||||
if (island == null || y < 0 || y >= 384) return biome("the_void");
|
if (island == null || y < 0 || y >= 384) return biome("the_void");
|
||||||
|
if (ExpansionOcean23.active(island)) {
|
||||||
|
var ocean = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (ocean.water() && y >= ocean.floorY() - 8) return biome(ExpansionOcean23.biome(island, ocean));
|
||||||
|
if (!ocean.water() && ocean.influence() > 0 && y >= ocean.floorY() - 8)
|
||||||
|
return biome(ExpansionOcean23.shoreBiome(island));
|
||||||
|
}
|
||||||
double scale = Math.clamp(island.diameter() / 3.0, 24.0, 144.0);
|
double scale = Math.clamp(island.diameter() / 3.0, 24.0, 144.0);
|
||||||
double localX = (x - island.centerX()) / scale, localZ = (z - island.centerZ()) / scale;
|
double localX = (x - island.centerX()) / scale, localZ = (z - island.centerZ()) / scale;
|
||||||
double variation = noise(island.seed(), localX, localZ);
|
double variation = noise(island.seed(), localX, localZ);
|
||||||
|
|||||||
+33
-3
@@ -97,7 +97,10 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
|||||||
return new Context(delegate, random);
|
return new Context(delegate, random);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) { ExpansionTerrain.apply(island, chunk); }
|
protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||||
|
if (ExpansionOcean23.active(island)) ExpansionOcean23.apply(island, chunk);
|
||||||
|
else ExpansionTerrain.apply(island, chunk);
|
||||||
|
}
|
||||||
public String journalDirectory() { return "sanctuary-expansion-v1"; }
|
public String journalDirectory() { return "sanctuary-expansion-v1"; }
|
||||||
public int rootGeneration() { return ExpansionIsland.GENERATION; }
|
public int rootGeneration() { return ExpansionIsland.GENERATION; }
|
||||||
public RandomState randomState(ExpansionIsland island) { return context(island).random(); }
|
public RandomState randomState(ExpansionIsland island) { return context(island).random(); }
|
||||||
@@ -125,6 +128,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
|||||||
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
|
fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
|
fr.koka.sanctuary.worldgen.transit.TransitStructures21.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
|
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);
|
||||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||||
}
|
}
|
||||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||||
@@ -134,11 +138,27 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
@Override public int getGenDepth() { return 384; }
|
@Override public int getGenDepth() { return 384; }
|
||||||
@Override public int getMinY() { return 0; }
|
@Override public int getMinY() { return 0; }
|
||||||
@Override public int getSeaLevel() { return -64; }
|
@Override public int getSeaLevel() {
|
||||||
|
Integer level = NativeStructurePlacement23.seaLevel();
|
||||||
|
if (level == null) level = ExpansionOcean23.decorationSeaLevel();
|
||||||
|
return level == null ? -64 : level;
|
||||||
|
}
|
||||||
|
@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; }
|
||||||
|
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
|
||||||
|
try { super.applyBiomeDecoration(level, chunk, structures); }
|
||||||
|
finally { ExpansionOcean23.restoreDecoration(previous); }
|
||||||
|
}
|
||||||
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
|
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
|
||||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState ignored) {
|
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState ignored) {
|
||||||
var island = islandAt(x, z);
|
var island = islandAt(x, z);
|
||||||
if (island == null) return getMinY();
|
if (island == null) return getMinY();
|
||||||
|
if (ExpansionOcean23.active(island)) {
|
||||||
|
var ocean = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (ocean.water()) return (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG
|
||||||
|
? ocean.floorY() : ocean.waterLevel()) + 1;
|
||||||
|
}
|
||||||
var context = context(island);
|
var context = context(island);
|
||||||
return context.generator().getBaseHeight(x, z, type, level, context.random());
|
return context.generator().getBaseHeight(x, z, type, level, context.random());
|
||||||
}
|
}
|
||||||
@@ -146,7 +166,17 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
|||||||
var island = islandAt(x, z);
|
var island = islandAt(x, z);
|
||||||
if (island != null) {
|
if (island != null) {
|
||||||
var context = context(island);
|
var context = context(island);
|
||||||
return context.generator().getBaseColumn(x, z, level, context.random());
|
var column = context.generator().getBaseColumn(x, z, level, context.random());
|
||||||
|
if (ExpansionOcean23.active(island)) {
|
||||||
|
var ocean = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (ocean.water()) {
|
||||||
|
for (int y = ocean.bottomY(); y <= ocean.floorY(); y++)
|
||||||
|
column.setBlock(y, Blocks.STONE.defaultBlockState());
|
||||||
|
for (int y = ocean.floorY() + 1; y <= ocean.waterLevel(); y++)
|
||||||
|
column.setBlock(y, Blocks.WATER.defaultBlockState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return column;
|
||||||
}
|
}
|
||||||
BlockState[] air = new BlockState[384];
|
BlockState[] air = new BlockState[384];
|
||||||
java.util.Arrays.fill(air, Blocks.AIR.defaultBlockState());
|
java.util.Arrays.fill(air, Blocks.AIR.defaultBlockState());
|
||||||
|
|||||||
@@ -48,15 +48,15 @@ public final class ExpansionCommands {
|
|||||||
.then(createTree("create", false)).then(createTree("preview", true))
|
.then(createTree("create", false)).then(createTree("preview", true))
|
||||||
.then(Commands.literal("quick").then(Commands.argument("direction", StringArgumentType.word())
|
.then(Commands.literal("quick").then(Commands.argument("direction", StringArgumentType.word())
|
||||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.DIRECTIONS, b))
|
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.DIRECTIONS, b))
|
||||||
.executes(c -> quick(c, null, 0, "natural"))
|
.executes(c -> quick(c, null, 0, null))
|
||||||
.then(Commands.argument("climate", StringArgumentType.word())
|
.then(Commands.argument("climate", StringArgumentType.word())
|
||||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.CLIMATES, b))
|
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.CLIMATES, b))
|
||||||
.executes(c -> quick(c, word(c, "climate"), 0, "natural"))
|
.executes(c -> quick(c, word(c, "climate"), 0, null))
|
||||||
.then(Commands.argument("diameter", IntegerArgumentType.integer(64, 1024))
|
.then(Commands.argument("diameter", IntegerArgumentType.integer(64, 1024))
|
||||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(List.of("64", "128", "256", "512", "1024"), b))
|
.suggests((c, b) -> SharedSuggestionProvider.suggest(List.of("64", "128", "256", "512", "1024"), b))
|
||||||
.executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"), "natural"))
|
.executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"), null))
|
||||||
.then(Commands.argument("relief", StringArgumentType.word())
|
.then(Commands.argument("relief", StringArgumentType.word())
|
||||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.RELIEFS, b))
|
.suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.reliefs(c.getSource().getLevel().getChunkSource().getGenerator() instanceof ExpansionChunkGenerator g ? g.rootGeneration() : 12), b))
|
||||||
.executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"), word(c, "relief")))
|
.executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"), word(c, "relief")))
|
||||||
.then(distanceArgument().executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"),
|
.then(distanceArgument().executes(c -> quick(c, word(c, "climate"), IntegerArgumentType.getInteger(c, "diameter"),
|
||||||
word(c, "relief"), IntegerArgumentType.getInteger(c, "distance")))))))));
|
word(c, "relief"), IntegerArgumentType.getInteger(c, "distance")))))))));
|
||||||
@@ -80,7 +80,7 @@ public final class ExpansionCommands {
|
|||||||
.then(Commands.argument("climate", StringArgumentType.word()).suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.CLIMATES, b))
|
.then(Commands.argument("climate", StringArgumentType.word()).suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.CLIMATES, b))
|
||||||
.then(Commands.argument("diameter", IntegerArgumentType.integer(64, 1024))
|
.then(Commands.argument("diameter", IntegerArgumentType.integer(64, 1024))
|
||||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(List.of("64", "128", "256", "512", "1024"), b))
|
.suggests((c, b) -> SharedSuggestionProvider.suggest(List.of("64", "128", "256", "512", "1024"), b))
|
||||||
.then(Commands.argument("relief", StringArgumentType.word()).suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.RELIEFS, b))
|
.then(Commands.argument("relief", StringArgumentType.word()).suggests((c, b) -> SharedSuggestionProvider.suggest(ExpansionIsland.reliefs(c.getSource().getLevel().getChunkSource().getGenerator() instanceof ExpansionChunkGenerator g ? g.rootGeneration() : 12), b))
|
||||||
.executes(c -> create(c, preview, 0))
|
.executes(c -> create(c, preview, 0))
|
||||||
.then(distanceArgument().executes(c -> create(c, preview, IntegerArgumentType.getInteger(c, "distance"))))))))));
|
.then(distanceArgument().executes(c -> create(c, preview, IntegerArgumentType.getInteger(c, "distance"))))))))));
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,10 @@ public final class ExpansionCommands {
|
|||||||
String id = "island_" + next;
|
String id = "island_" + next;
|
||||||
int chosenDiameter = diameter != 0 ? diameter : ExpansionIsland.spatialGeneration(session.islands().getFirst().generation())
|
int chosenDiameter = diameter != 0 ? diameter : ExpansionIsland.spatialGeneration(session.islands().getFirst().generation())
|
||||||
? ExpansionRandomSize.choose(c.getSource().getLevel().getSeed(), id) : 128;
|
? ExpansionRandomSize.choose(c.getSource().getLevel().getSeed(), id) : 128;
|
||||||
var island = session.create(id, "sanctuary", direction,
|
String chosenClimate = climate == null ? ExpansionIsland.directionalClimate(direction) : climate;
|
||||||
climate == null ? ExpansionIsland.directionalClimate(direction) : climate, chosenDiameter, relief, minimumDistance);
|
String chosenRelief = relief == null ? ExpansionOcean23.quickRelief(c.getSource().getLevel().getSeed(), id,
|
||||||
|
chosenDiameter, chosenClimate, session.islands().getFirst().generation()) : relief;
|
||||||
|
var island = session.create(id, "sanctuary", direction, chosenClimate, chosenDiameter, chosenRelief, minimumDistance);
|
||||||
describe(c, island, false);
|
describe(c, island, false);
|
||||||
return Command.SINGLE_SUCCESS;
|
return Command.SINGLE_SUCCESS;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ public record ExpansionDensity(DensityFunction source, ExpansionIsland island) i
|
|||||||
// Fractional Y elsewhere is interpolated, avoiding one-block terraces from rounding.
|
// Fractional Y elsewhere is interpolated, avoiding one-block terraces from rounding.
|
||||||
float density = fraction == 0.0 ? low : (float) (low
|
float density = fraction == 0.0 ? low : (float) (low
|
||||||
+ (original.sampleValue(sample, sx, lowerY + 1, sz) - low) * fraction);
|
+ (original.sampleValue(sample, sx, lowerY + 1, sz) - low) * fraction);
|
||||||
return ExpansionRelief.sculpt(island, column, y, Math.clamp(density, -1.0F, 1.0F));
|
float shaped = ExpansionRelief.sculpt(island, column, y, Math.clamp(density, -1.0F, 1.0F));
|
||||||
|
return ExpansionOcean23.active(island)
|
||||||
|
? ExpansionOcean23.density(COLUMNS.get().ocean(island, x, z), y, shaped) : shaped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||||
@@ -58,6 +60,7 @@ public record ExpansionDensity(DensityFunction source, ExpansionIsland island) i
|
|||||||
private ExpansionIsland selected;
|
private ExpansionIsland selected;
|
||||||
private final int[] xs = new int[SIZE], zs = new int[SIZE];
|
private final int[] xs = new int[SIZE], zs = new int[SIZE];
|
||||||
private final ExpansionRelief.Column[] columns = new ExpansionRelief.Column[SIZE];
|
private final ExpansionRelief.Column[] columns = new ExpansionRelief.Column[SIZE];
|
||||||
|
private final ExpansionOcean23.Column[] oceans = new ExpansionOcean23.Column[SIZE];
|
||||||
|
|
||||||
ExpansionRelief.Column get(ExpansionIsland island, int x, int z) {
|
ExpansionRelief.Column get(ExpansionIsland island, int x, int z) {
|
||||||
if (selected != island) {
|
if (selected != island) {
|
||||||
@@ -68,9 +71,15 @@ public record ExpansionDensity(DensityFunction source, ExpansionIsland island) i
|
|||||||
int slot = (hash ^ (hash >>> 16)) & (SIZE - 1);
|
int slot = (hash ^ (hash >>> 16)) & (SIZE - 1);
|
||||||
if (columns[slot] == null || xs[slot] != x || zs[slot] != z) {
|
if (columns[slot] == null || xs[slot] != x || zs[slot] != z) {
|
||||||
columns[slot] = ExpansionRelief.column(island, x, z);
|
columns[slot] = ExpansionRelief.column(island, x, z);
|
||||||
|
oceans[slot] = ExpansionOcean23.active(island) ? ExpansionOcean23.column(island, x, z) : null;
|
||||||
xs[slot] = x; zs[slot] = z;
|
xs[slot] = x; zs[slot] = z;
|
||||||
}
|
}
|
||||||
return columns[slot];
|
return columns[slot];
|
||||||
}
|
}
|
||||||
|
ExpansionOcean23.Column ocean(ExpansionIsland island, int x, int z) {
|
||||||
|
get(island, x, z);
|
||||||
|
int hash = x * 0x9e3779b9 ^ Integer.rotateLeft(z * 0x85ebca6b, 13);
|
||||||
|
return oceans[(hash ^ (hash >>> 16)) & (SIZE - 1)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import java.util.List;
|
|||||||
|
|
||||||
/** Immutable alpha.12 region contract. Directions place islands; climate remains an explicit choice. */
|
/** Immutable alpha.12 region contract. Directions place islands; climate remains an explicit choice. */
|
||||||
public record ExpansionIsland(String id, String parent, String direction, String climate,
|
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) {
|
int diameter, int centerX, int centerZ, long seed, String relief, String status, int generation, String reuseToken,
|
||||||
|
List<String> structurePriorities) {
|
||||||
public static final int GENERATION = 12;
|
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> 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");
|
public static final List<String> CLIMATES = List.of("temperate", "boreal", "glacial", "cold_humid", "humid", "tropical", "warm", "arid", "dry", "cold_dry", "autumn", "cherry", "lost_city");
|
||||||
public static final List<String> RELIEFS = List.of("natural", "peaks", "canyon", "volcano");
|
public static final List<String> RELIEFS = List.of("natural", "peaks", "canyon", "volcano");
|
||||||
|
public static final List<String> RELIEFS23 = List.of("natural", "peaks", "canyon", "volcano", "ocean");
|
||||||
public static final List<Integer> LEGACY_DIAMETERS = List.of(64, 128, 256, 512);
|
public static final List<Integer> LEGACY_DIAMETERS = List.of(64, 128, 256, 512);
|
||||||
public static final List<Integer> DIAMETERS = List.of(64, 128, 256, 512, 1024);
|
public static final List<Integer> DIAMETERS = List.of(64, 128, 256, 512, 1024);
|
||||||
public static final int DECORATION_MARGIN = 32;
|
public static final int DECORATION_MARGIN = 32;
|
||||||
@@ -27,23 +29,38 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
|||||||
Codec.STRING.fieldOf("relief").forGetter(ExpansionIsland::relief),
|
Codec.STRING.fieldOf("relief").forGetter(ExpansionIsland::relief),
|
||||||
Codec.STRING.fieldOf("status").forGetter(ExpansionIsland::status),
|
Codec.STRING.fieldOf("status").forGetter(ExpansionIsland::status),
|
||||||
Codec.INT.fieldOf("generation").forGetter(ExpansionIsland::generation),
|
Codec.INT.fieldOf("generation").forGetter(ExpansionIsland::generation),
|
||||||
Codec.STRING.optionalFieldOf("reuse_token", "").forGetter(ExpansionIsland::reuseToken)
|
Codec.STRING.optionalFieldOf("reuse_token", "").forGetter(ExpansionIsland::reuseToken),
|
||||||
|
Codec.STRING.listOf().optionalFieldOf("structure_priorities", List.of()).forGetter(ExpansionIsland::structurePriorities)
|
||||||
).apply(i, ExpansionIsland::new));
|
).apply(i, ExpansionIsland::new));
|
||||||
|
|
||||||
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
|
public ExpansionIsland(String id, String parent, String direction, String climate, int diameter,
|
||||||
int centerX, int centerZ, long seed, String relief, String status, int generation) {
|
int centerX, int centerZ, long seed, String relief, String status, int generation) {
|
||||||
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, "");
|
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, "", 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) {
|
||||||
|
this(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExpansionIsland {
|
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 (!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.contains(relief)) throw new IllegalArgumentException("Unknown island profile");
|
if (!DIRECTIONS.contains(direction) || !CLIMATES.contains(climate) || !reliefs(generation).contains(relief)) throw new IllegalArgumentException("Unknown island profile");
|
||||||
|
if (relief.equals("ocean") && (generation != 23 || 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 (!((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");
|
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");
|
||||||
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
|
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
|
||||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20, 21 or 22 expansion");
|
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16, 17, 19, 20, 21 or 22 expansion");
|
||||||
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
||||||
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
||||||
|
if (structurePriorities == null || structurePriorities.size() > 64
|
||||||
|
|| structurePriorities.stream().anyMatch(value -> value == null || !value.matches("[a-z0-9_./:-]{1,128}")))
|
||||||
|
throw new IllegalArgumentException("Invalid structure priority profile");
|
||||||
|
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 || id.equals("sanctuary")))
|
||||||
|
throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ExpansionIsland origin(int diameter, long seed) {
|
public static ExpansionIsland origin(int diameter, long seed) {
|
||||||
@@ -53,7 +70,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);
|
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; }
|
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; }
|
||||||
|
public static List<String> reliefs(int generation) { return generation == 23 ? RELIEFS23 : RELIEFS; }
|
||||||
public boolean origin() { return id.equals("sanctuary"); }
|
public boolean origin() { return id.equals("sanctuary"); }
|
||||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||||
@@ -71,8 +89,11 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
|||||||
return Math.abs((long) centerX - other.centerX) <= reservedRadius() + other.reservedRadius()
|
return Math.abs((long) centerX - other.centerX) <= reservedRadius() + other.reservedRadius()
|
||||||
&& Math.abs((long) centerZ - other.centerZ) <= 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); }
|
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); }
|
public ExpansionIsland withReuseToken(String token) { return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, token, structurePriorities); }
|
||||||
|
public ExpansionIsland withStructurePriorities(List<String> priorities) {
|
||||||
|
return new ExpansionIsland(id, parent, direction, climate, diameter, centerX, centerZ, seed, relief, status, generation, reuseToken, priorities);
|
||||||
|
}
|
||||||
public static String directionalClimate(String direction) {
|
public static String directionalClimate(String direction) {
|
||||||
return switch (direction) {
|
return switch (direction) {
|
||||||
case "north" -> "boreal"; case "northeast" -> "cold_humid"; case "east" -> "humid";
|
case "north" -> "boreal"; case "northeast" -> "cold_humid"; case "east" -> "humid";
|
||||||
|
|||||||
@@ -160,11 +160,13 @@ public final class ExpansionJournal {
|
|||||||
JsonObject entry = element.getAsJsonObject();
|
JsonObject entry = element.getAsJsonObject();
|
||||||
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
|
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
|
||||||
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
|
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
|
||||||
|
if (rootGeneration == 23 && entry.has("structure_priorities")) fields.add("structure_priorities");
|
||||||
requireFields(entry, fields);
|
requireFields(entry, fields);
|
||||||
decoded.add(new ExpansionIsland(string(entry, "id"), string(entry, "parent"), string(entry, "direction"), string(entry, "climate"),
|
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(),
|
integer(entry, "diameter"), integer(entry, "center_x"), integer(entry, "center_z"), number(entry, "seed").longValueExact(),
|
||||||
string(entry, "relief"), string(entry, "status"), integer(entry, "generation"),
|
string(entry, "relief"), string(entry, "status"), integer(entry, "generation"),
|
||||||
entry.has("reuse_token") ? string(entry, "reuse_token") : ""));
|
entry.has("reuse_token") ? string(entry, "reuse_token") : "",
|
||||||
|
entry.has("structure_priorities") ? priorities(entry) : List.of()));
|
||||||
}
|
}
|
||||||
return validate(decoded);
|
return validate(decoded);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
@@ -220,6 +222,10 @@ public final class ExpansionJournal {
|
|||||||
entry.addProperty("seed", island.seed()); entry.addProperty("relief", island.relief()); entry.addProperty("status", island.status());
|
entry.addProperty("seed", island.seed()); entry.addProperty("relief", island.relief()); entry.addProperty("status", island.status());
|
||||||
entry.addProperty("generation", island.generation());
|
entry.addProperty("generation", island.generation());
|
||||||
if (!island.reuseToken().isEmpty()) entry.addProperty("reuse_token", island.reuseToken());
|
if (!island.reuseToken().isEmpty()) entry.addProperty("reuse_token", island.reuseToken());
|
||||||
|
if (!island.structurePriorities().isEmpty()) {
|
||||||
|
var priorities = new JsonArray(); island.structurePriorities().forEach(priorities::add);
|
||||||
|
entry.add("structure_priorities", priorities);
|
||||||
|
}
|
||||||
array.add(entry);
|
array.add(entry);
|
||||||
}
|
}
|
||||||
root.add("islands", array);
|
root.add("islands", array);
|
||||||
@@ -258,6 +264,17 @@ public final class ExpansionJournal {
|
|||||||
private static void requireFields(JsonObject value, Set<String> fields) throws IOException {
|
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");
|
if (!value.keySet().equals(fields)) throw new IOException("Missing or unknown expansion journal fields");
|
||||||
}
|
}
|
||||||
|
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");
|
||||||
|
var values = new ArrayList<String>();
|
||||||
|
for (var entry : array.getAsJsonArray()) {
|
||||||
|
if (!entry.isJsonPrimitive() || !entry.getAsJsonPrimitive().isString())
|
||||||
|
throw new IOException("Structure priority family must be a string");
|
||||||
|
values.add(entry.getAsString());
|
||||||
|
}
|
||||||
|
return List.copyOf(values);
|
||||||
|
}
|
||||||
private static String string(JsonObject value, String name) throws IOException {
|
private static String string(JsonObject value, String name) throws IOException {
|
||||||
JsonPrimitive primitive = value.getAsJsonPrimitive(name);
|
JsonPrimitive primitive = value.getAsJsonPrimitive(name);
|
||||||
if (!primitive.isString()) throw new IOException("Expected string field: " + name);
|
if (!primitive.isString()) throw new IOException("Expected string field: " + name);
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/** One immutable geographic field for the rock, water, biomes and native ocean structures. */
|
||||||
|
public final class ExpansionOcean23 {
|
||||||
|
private static final ThreadLocal<Integer> DECORATION_SEA_LEVEL = new ThreadLocal<>();
|
||||||
|
public static Integer decorationSeaLevel() { return DECORATION_SEA_LEVEL.get(); }
|
||||||
|
public static Integer enterDecoration(int seaLevel) {
|
||||||
|
Integer previous = DECORATION_SEA_LEVEL.get(); DECORATION_SEA_LEVEL.set(seaLevel); return previous;
|
||||||
|
}
|
||||||
|
public static void restoreDecoration(Integer previous) {
|
||||||
|
if (previous == null) DECORATION_SEA_LEVEL.remove(); else DECORATION_SEA_LEVEL.set(previous);
|
||||||
|
}
|
||||||
|
public static final List<String> BIOMES = List.of("ocean", "deep_ocean", "cold_ocean", "deep_cold_ocean",
|
||||||
|
"frozen_ocean", "deep_frozen_ocean", "warm_ocean", "lukewarm_ocean", "deep_lukewarm_ocean", "beach", "snowy_beach", "stony_shore");
|
||||||
|
public record Column(int waterLevel, int floorY, boolean water, boolean deep, int bottomY, double influence) {
|
||||||
|
public int depth() { return water ? waterLevel - floorY : 0; }
|
||||||
|
}
|
||||||
|
private ExpansionOcean23() {}
|
||||||
|
public static boolean active(ExpansionIsland island) {
|
||||||
|
return island != null && island.generation() == 23 && !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 || 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) {
|
||||||
|
int level = seaLevel(island);
|
||||||
|
if (!active(island)) return new Column(level, level, false, false, level, 0);
|
||||||
|
double radius = island.radius();
|
||||||
|
double angle = unit(island.seed() ^ 0x2310CEAL) * Math.PI * 2;
|
||||||
|
double dx = (x - island.centerX()) / radius, dz = (z - island.centerZ()) / radius;
|
||||||
|
double u = dx * Math.cos(angle) + dz * Math.sin(angle) - .09;
|
||||||
|
double v = -dx * Math.sin(angle) + dz * Math.cos(angle) + .035;
|
||||||
|
double phase = unit(island.seed() ^ 0x231BA51L) * Math.PI * 2;
|
||||||
|
double theta = Math.atan2(v, u);
|
||||||
|
double coast = 1 + .08 * Math.sin(theta * 3 + phase) + .055 * Math.sin(theta * 5 - phase)
|
||||||
|
+ .025 * Math.sin(theta * 9 + phase * 2);
|
||||||
|
double radial = Math.hypot(u / .64, v / .59) / coast;
|
||||||
|
double influence = 1 - smooth((radial - 1.08) / .14);
|
||||||
|
if (influence <= 0 || !island.contains(x, z)) return new Column(level, level, false, false, level, 0);
|
||||||
|
double depth = 48 * smooth((1 - radial) / .48)
|
||||||
|
+ 6 * smooth((1 - radial) / .3) * Math.sin(u * 17 + phase) * Math.cos(v * 13 - phase);
|
||||||
|
int floor = radial <= 1 ? level - Math.max(1, (int) Math.round(depth))
|
||||||
|
: level + (int) Math.round(4 + 24 * smooth((radial - 1) / .18));
|
||||||
|
int shell = 14 + (int) Math.round(3 * (1 + Math.sin(u * 11 + v * 7 + phase)));
|
||||||
|
boolean water = radial <= 1;
|
||||||
|
return new Column(level, floor, water, water && level - floor >= 32, floor - shell, influence);
|
||||||
|
}
|
||||||
|
public static float density(Column c, int y, float natural) {
|
||||||
|
if (c.influence() <= 0 || y < c.bottomY() - 2) return natural;
|
||||||
|
// Rock remains a bounded geological shell. The original lower caves survive below it.
|
||||||
|
float shaped = y <= c.floorY() ? Math.min(1, (y - c.bottomY() + 2) / 2f) : -1;
|
||||||
|
return (float) (natural + (shaped - natural) * c.influence());
|
||||||
|
}
|
||||||
|
public static boolean retains(Column c, int y) {
|
||||||
|
return c.water() && y <= c.floorY() && y >= c.bottomY();
|
||||||
|
}
|
||||||
|
public static String shoreBiome(ExpansionIsland island) {
|
||||||
|
return switch (island.climate()) {
|
||||||
|
case "glacial", "cold_dry" -> "snowy_beach";
|
||||||
|
case "dry" -> "stony_shore";
|
||||||
|
default -> "beach";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static String biome(ExpansionIsland island, Column column) {
|
||||||
|
return switch (island.climate()) {
|
||||||
|
case "glacial" -> column.deep() ? "deep_frozen_ocean" : "frozen_ocean";
|
||||||
|
case "boreal", "cold_humid", "cold_dry" -> column.deep() ? "deep_cold_ocean" : "cold_ocean";
|
||||||
|
case "warm", "tropical", "arid" -> column.deep() ? "deep_lukewarm_ocean"
|
||||||
|
: column.depth() < 16 ? "warm_ocean" : "lukewarm_ocean";
|
||||||
|
default -> column.deep() ? "deep_ocean" : "ocean";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/** Exactly one chunk-local fill, before features. Static source water does not schedule an ocean of ticks. */
|
||||||
|
public static void apply(ExpansionIsland island, ChunkAccess chunk) {
|
||||||
|
if (!active(island)) return;
|
||||||
|
var pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++)
|
||||||
|
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
|
||||||
|
Column c = column(island, x, z);
|
||||||
|
if (!c.water()) continue;
|
||||||
|
// Surface rules run before this pass. Pin the same floor used by admission and biomes.
|
||||||
|
for (int y = c.bottomY(); y <= c.floorY(); y++) {
|
||||||
|
pos.set(x, y, z);
|
||||||
|
var rock = y >= c.floorY() - 1 ? (c.deep() ? Blocks.GRAVEL : Blocks.SAND) : Blocks.STONE;
|
||||||
|
chunk.setBlockState(pos, rock.defaultBlockState(), 0);
|
||||||
|
}
|
||||||
|
for (int y = c.floorY() + 1; y <= c.waterLevel(); y++)
|
||||||
|
chunk.setBlockState(pos.set(x, y, z), Blocks.WATER.defaultBlockState(), 0);
|
||||||
|
}
|
||||||
|
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
|
||||||
|
Heightmap.Types.OCEAN_FLOOR_WG, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||||
|
}
|
||||||
|
private static double smooth(double x) { x = Math.clamp(x, 0, 1); return x * x * (3 - 2 * x); }
|
||||||
|
private static double unit(long v) {
|
||||||
|
v = (v ^ v >>> 30) * 0xbf58476d1ce4e5b9L;
|
||||||
|
v = (v ^ v >>> 27) * 0x94d049bb133111ebL;
|
||||||
|
return ((v ^ v >>> 31) >>> 11) * 0x1.0p-53;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ public final class ExpansionPlacement {
|
|||||||
if (direction == null || !ExpansionIsland.DIRECTIONS.contains(direction)) throw new IllegalArgumentException("invalid_direction");
|
if (direction == null || !ExpansionIsland.DIRECTIONS.contains(direction)) throw new IllegalArgumentException("invalid_direction");
|
||||||
if (climate == null || !ExpansionIsland.CLIMATES.contains(climate)) throw new IllegalArgumentException("invalid_climate");
|
if (climate == null || !ExpansionIsland.CLIMATES.contains(climate)) throw new IllegalArgumentException("invalid_climate");
|
||||||
if (!ExpansionIsland.DIAMETERS.contains(diameter)) throw new IllegalArgumentException("invalid_diameter");
|
if (!ExpansionIsland.DIAMETERS.contains(diameter)) throw new IllegalArgumentException("invalid_diameter");
|
||||||
if (relief == null || !ExpansionIsland.RELIEFS.contains(relief)) throw new IllegalArgumentException("invalid_relief");
|
if (relief == null || !ExpansionIsland.reliefs(islands.getFirst().generation()).contains(relief)) throw new IllegalArgumentException("invalid_relief");
|
||||||
validateDistance(minimumDistance);
|
validateDistance(minimumDistance);
|
||||||
int rootGeneration = islands.getFirst().generation();
|
int rootGeneration = islands.getFirst().generation();
|
||||||
int generation = ExpansionIsland.spatialGeneration(rootGeneration) ? rootGeneration : ExpansionIsland.GENERATION;
|
int generation = ExpansionIsland.spatialGeneration(rootGeneration) ? rootGeneration : ExpansionIsland.GENERATION;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public final class ExpansionRelief {
|
|||||||
double uplift;
|
double uplift;
|
||||||
double canyon = 0.0, crater = 0.0;
|
double canyon = 0.0, crater = 0.0;
|
||||||
switch (island.relief()) {
|
switch (island.relief()) {
|
||||||
case "natural" -> {
|
case "natural", "ocean" -> {
|
||||||
double ridge = Math.pow(1.0 - Math.abs(broad), 4.0);
|
double ridge = Math.pow(1.0 - Math.abs(broad), 4.0);
|
||||||
uplift = Math.clamp(12.0 * broad + 5.0 * detail + 10.0 * Math.max(0.0, ridge - 0.5), -20.0, 20.0);
|
uplift = Math.clamp(12.0 * broad + 5.0 * detail + 10.0 * Math.max(0.0, ridge - 0.5), -20.0, 20.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,10 +107,17 @@ public final class ExpansionRuntime {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
|
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
|
||||||
var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance);
|
// 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
|
||||||
|
? 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().map(value -> value.withStructurePriorities(priorities)).toList();
|
||||||
|
boolean nativeRefused = false;
|
||||||
for (var value : candidates) {
|
for (var value : candidates) {
|
||||||
ExpansionAdmission.checkDeadline(deadline);
|
ExpansionAdmission.checkDeadline(deadline);
|
||||||
try {
|
try {
|
||||||
|
if (value.generation() == 23) ExpansionStructures23.verifyExpansion(level, value, deadline);
|
||||||
var inspection = ExpansionIsland.spatialGeneration(value.generation()) ? ExpansionVoidReuse.inspect(level, value, deadline) : null;
|
var inspection = ExpansionIsland.spatialGeneration(value.generation()) ? ExpansionVoidReuse.inspect(level, value, deadline) : null;
|
||||||
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
|
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
|
||||||
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
|
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
|
||||||
@@ -127,12 +134,14 @@ public final class ExpansionRuntime {
|
|||||||
return admitted;
|
return admitted;
|
||||||
} catch (ExpansionAdmission.OccupiedException occupied) {
|
} catch (ExpansionAdmission.OccupiedException occupied) {
|
||||||
// IO failures, timeouts and unsuitable terrain stop this request without a new draw.
|
// IO failures, timeouts and unsuitable terrain stop this request without a new draw.
|
||||||
|
} catch (ExpansionStructures23.NoSuitableStructureException unsuitable) {
|
||||||
|
nativeRefused = true; // Try another location with the same immutable priorities.
|
||||||
} finally {
|
} finally {
|
||||||
generator.discardUnpublishedContext(value);
|
generator.discardUnpublishedContext(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExpansionAdmission.checkDeadline(deadline);
|
ExpansionAdmission.checkDeadline(deadline);
|
||||||
throw new IllegalArgumentException("no_free_space");
|
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
|
||||||
}
|
}
|
||||||
public void resume(String id) {
|
public void resume(String id) {
|
||||||
requireHealthyJournal();
|
requireHealthyJournal();
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** Exact built-in 26.3-pre-2 catalogue. Other dimensions remain native and ancient cities await Cavernes. */
|
||||||
|
public final class ExpansionStructureCatalog23 {
|
||||||
|
public enum Availability { OVERWORLD, CAVERNS_ONLY, NETHER_ONLY, END_ONLY }
|
||||||
|
public enum Habitat { SURFACE, UNDERGROUND, OCEAN, SHORE }
|
||||||
|
public record Entry(String id, String family, Availability availability, Habitat habitat) {}
|
||||||
|
private static final List<Entry> ENTRIES = create();
|
||||||
|
private static final Map<String, Entry> BY_ID = ENTRIES.stream().collect(Collectors.toUnmodifiableMap(Entry::id, Function.identity()));
|
||||||
|
private ExpansionStructureCatalog23() {}
|
||||||
|
public static List<Entry> entries() { return ENTRIES; }
|
||||||
|
public static Entry entry(String id) { return BY_ID.get(id); }
|
||||||
|
public static String family(String id) { Entry entry = entry(id); return entry == null ? "unknown" : entry.family(); }
|
||||||
|
private static List<Entry> create() {
|
||||||
|
var entries = new ArrayList<Entry>();
|
||||||
|
add(entries, "village", Habitat.SURFACE, "village_plains", "village_desert", "village_savanna", "village_snowy", "village_taiga");
|
||||||
|
for (String biome : List.of("bamboo_jungle", "birch_forest", "cherry_grove", "dappled_forest", "flower_forest", "forest", "meadow",
|
||||||
|
"old_growth_birch_forest", "old_growth_pine_taiga", "old_growth_spruce_taiga", "pale_garden", "savanna", "snowy_taiga",
|
||||||
|
"sparse_jungle", "swamp", "taiga", "windswept_forest", "wooded_badlands"))
|
||||||
|
add(entries, "camp", Habitat.SURFACE, "abandoned_camp_" + biome);
|
||||||
|
add(entries, "ruined_portal", Habitat.SURFACE, "ruined_portal", "ruined_portal_desert", "ruined_portal_jungle", "ruined_portal_mountain", "ruined_portal_swamp");
|
||||||
|
add(entries, "ruined_portal", Habitat.OCEAN, "ruined_portal_ocean");
|
||||||
|
add(entries, "mineshaft", Habitat.UNDERGROUND, "mineshaft", "mineshaft_mesa");
|
||||||
|
add(entries, "ocean_ruin", Habitat.OCEAN, "ocean_ruin_cold", "ocean_ruin_warm");
|
||||||
|
add(entries, "shipwreck", Habitat.OCEAN, "shipwreck");
|
||||||
|
add(entries, "shipwreck", Habitat.SHORE, "shipwreck_beached");
|
||||||
|
add(entries, "buried_treasure", Habitat.SHORE, "buried_treasure");
|
||||||
|
for (String name : List.of("desert_pyramid", "igloo", "jungle_pyramid", "mansion", "pillager_outpost", "swamp_hut", "trail_ruins"))
|
||||||
|
add(entries, name, Habitat.SURFACE, name);
|
||||||
|
add(entries, "monument", Habitat.OCEAN, "monument");
|
||||||
|
add(entries, "stronghold", Habitat.UNDERGROUND, "stronghold");
|
||||||
|
add(entries, "trial_chambers", Habitat.UNDERGROUND, "trial_chambers");
|
||||||
|
entries.add(new Entry("minecraft:ancient_city", "ancient_city", Availability.CAVERNS_ONLY, Habitat.UNDERGROUND));
|
||||||
|
for (String name : List.of("fortress", "bastion_remnant", "nether_fossil", "ruined_portal_nether"))
|
||||||
|
entries.add(new Entry("minecraft:" + name, name.equals("ruined_portal_nether") ? "ruined_portal" : name, Availability.NETHER_ONLY, Habitat.UNDERGROUND));
|
||||||
|
entries.add(new Entry("minecraft:end_city", "end_city", Availability.END_ONLY, Habitat.SURFACE));
|
||||||
|
return List.copyOf(entries);
|
||||||
|
}
|
||||||
|
private static void add(List<Entry> entries, String family, Habitat habitat, String... names) {
|
||||||
|
for (String name : names) entries.add(new Entry("minecraft:" + name, family, Availability.OVERWORLD, habitat));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,10 @@ public final class ExpansionStructures {
|
|||||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
|
if (generator.rootGeneration() == 23) {
|
||||||
|
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!manager.shouldGenerateStructures()) return;
|
if (!manager.shouldGenerateStructures()) return;
|
||||||
int blockX = chunk.getPos().getMiddleBlockX(), blockZ = chunk.getPos().getMiddleBlockZ();
|
int blockX = chunk.getPos().getMiddleBlockX(), blockZ = chunk.getPos().getMiddleBlockZ();
|
||||||
ExpansionIsland island = generator.islandAt(blockX, blockZ);
|
ExpansionIsland island = generator.islandAt(blockX, blockZ);
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructureCatalog23.Availability;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructureCatalog23.Entry;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructureCatalog23.Habitat;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
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 java.util.function.Predicate;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
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.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
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.chunk.status.ChunkStatus;
|
||||||
|
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.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;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.StrongholdStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
/** Native structures on generation23 islands: deterministic candidate cells, native pieces and native starts.
|
||||||
|
* Nothing is placed or persisted by the preflight. A refused start is never published as a reference.
|
||||||
|
*/
|
||||||
|
public final class ExpansionStructures23 {
|
||||||
|
public static final int CELL_CHUNKS = 8;
|
||||||
|
private static final int MAX_NATIVE_ATTEMPTS_PER_SITE = 12;
|
||||||
|
private static final int MAX_COLUMN_SAMPLES = 4096;
|
||||||
|
private static final boolean DIAGNOSTICS = Boolean.getBoolean("sanctuary.expansion.structureDiagnostics")
|
||||||
|
|| Boolean.getBoolean("sanctuary.test.expansion");
|
||||||
|
private 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; }
|
||||||
|
public record Candidate(ChunkPos chunk, long roll) {}
|
||||||
|
public record Accepted(Holder<Structure> structure, StructureStart start) {}
|
||||||
|
public static final class NoSuitableStructureException extends IllegalArgumentException {
|
||||||
|
public NoSuitableStructureException() { super("No suitable native structure fits this expansion; choose another seed, climate, relief or larger size"); }
|
||||||
|
}
|
||||||
|
private static final class SampleBudgetExceeded extends RuntimeException {}
|
||||||
|
|
||||||
|
/** Includes one deterministic central candidate even for a diameter64 island. Never draws from mutable server history. */
|
||||||
|
public static List<Candidate> candidates(ExpansionIsland island) {
|
||||||
|
var result = new LinkedHashMap<Long, Candidate>();
|
||||||
|
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)));
|
||||||
|
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);
|
||||||
|
if (Long.remainderUnsigned(roll, 100) >= 68) continue;
|
||||||
|
var pos = new ChunkPos(rootX + cx * CELL_CHUNKS + (int) Long.remainderUnsigned(roll >>> 8, CELL_CHUNKS),
|
||||||
|
rootZ + cz * CELL_CHUNKS + (int) Long.remainderUnsigned(roll >>> 17, CELL_CHUNKS));
|
||||||
|
if (island.contains(pos.getMiddleBlockX(), pos.getMiddleBlockZ())) result.putIfAbsent(pos.pack(), new Candidate(pos, roll));
|
||||||
|
}
|
||||||
|
return List.copyOf(result.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||||
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||||
|
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
|
||||||
|
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||||
|
if (island == null || island.generation() != 23) 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;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
manager.setStartForStructure(accepted.structure().value(), accepted.start(), chunk);
|
||||||
|
diagnostic(island, candidate, "accepted", accepted.structure().unwrapKey().orElseThrow().identifier().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounded, read-only native preflight used before reservation of a generation23 expansion.
|
||||||
|
* The same descriptor, cell order, templates and support predicate are replayed by createStructures.
|
||||||
|
* No active source snapshot is mutated and no real world chunk is requested.
|
||||||
|
*/
|
||||||
|
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
|
||||||
|
if (island.generation() != 23 || island.origin() || !level.structureManager().shouldGenerateStructures()) 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());
|
||||||
|
NativeView view = new NativeView(host, island, isolated, level.registryAccess());
|
||||||
|
var factory = PalettedContainerFactory.create(level.registryAccess());
|
||||||
|
for (Candidate candidate : candidates(island)) {
|
||||||
|
checkDeadline(deadlineNanos);
|
||||||
|
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, factory, null);
|
||||||
|
Accepted accepted = plan(host, view, island, candidate, level.registryAccess(), level.getStructureTemplateManager(), level.dimension(), chunk, deadlineNanos);
|
||||||
|
if (accepted != null) return;
|
||||||
|
}
|
||||||
|
throw new NoSuitableStructureException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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); }
|
||||||
|
catch (SampleBudgetExceeded refused) { diagnostic(island, candidate, "terrain-budget-refused", ""); return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pure type-specific diagnostic, using the production admission rules and no live chunk generation. */
|
||||||
|
public static Accepted preview(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos) {
|
||||||
|
return preview(level, island, structureId, deadlineNanos, false);
|
||||||
|
}
|
||||||
|
/** Finds a site that the full production selection actually chooses, useful for full lifecycle diagnostics. */
|
||||||
|
public static Accepted previewSelected(ServerLevel level, ExpansionIsland island, String structureId, long deadlineNanos) {
|
||||||
|
return preview(level, island, structureId, deadlineNanos, true);
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
var view = new NativeView(host, island, source, level.registryAccess());
|
||||||
|
var factory = PalettedContainerFactory.create(level.registryAccess());
|
||||||
|
for (Candidate candidate : candidates(island)) {
|
||||||
|
checkDeadline(deadlineNanos);
|
||||||
|
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));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
RandomState random = view.randomState(island);
|
||||||
|
GroundSampler ground = new GroundSampler(view, chunk, random, deadlineNanos);
|
||||||
|
int x = candidate.chunk().getMiddleBlockX(), z = candidate.chunk().getMiddleBlockZ();
|
||||||
|
Ground centre = ground.at(x, z);
|
||||||
|
if (centre.top < 32) return null;
|
||||||
|
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||||
|
var resolver = view.getBiomeSource().createResolver(sampler);
|
||||||
|
int undergroundY = Math.clamp(centre.top - 60, 50, 210);
|
||||||
|
Holder<Biome> surfaceBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, centre.top >> 2, z >> 2));
|
||||||
|
Holder<Biome> undergroundBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, undergroundY >> 2, z >> 2));
|
||||||
|
int seaLevel = ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : -64;
|
||||||
|
Holder<Biome> waterBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, Math.max(0, seaLevel - 8) >> 2, z >> 2));
|
||||||
|
var registry = registries.lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var eligible = new ArrayList<Choice>();
|
||||||
|
for (Entry entry : catalogue()) {
|
||||||
|
if (entry.availability() != Availability.OVERWORLD || !sizeEligible(entry, island) || !included.test(entry)) continue;
|
||||||
|
Holder<Structure> holder = registry.get(Identifier.parse(entry.id())).orElse(null);
|
||||||
|
if (holder == null) continue;
|
||||||
|
Holder<Biome> biome = entry.habitat() == Habitat.UNDERGROUND ? undergroundBiome
|
||||||
|
: entry.habitat() == Habitat.OCEAN ? waterBiome : surfaceBiome;
|
||||||
|
if (!holder.value().biomes().contains(biome)) continue;
|
||||||
|
if (entry.habitat() == Habitat.OCEAN && !ExpansionOcean23.column(island, x, z).water()) continue;
|
||||||
|
long salt = mix(candidate.roll() ^ entry.id().hashCode() * 0x9e3779b97f4a7c15L);
|
||||||
|
double unit = ((salt >>> 11) + 1.0) * 0x1.0p-53;
|
||||||
|
int weight = weight(entry.family()) * (island.structurePriorities().contains(entry.family()) ? 4 : 1);
|
||||||
|
eligible.add(new Choice(entry, holder, -Math.log(unit) / weight));
|
||||||
|
}
|
||||||
|
eligible.sort(Comparator.comparingDouble(Choice::score).thenComparing(choice -> choice.entry().id()));
|
||||||
|
int attempts = 0;
|
||||||
|
for (Choice choice : eligible) {
|
||||||
|
if (++attempts > MAX_NATIVE_ATTEMPTS_PER_SITE) break;
|
||||||
|
checkDeadline(deadlineNanos);
|
||||||
|
Structure original = choice.holder().value();
|
||||||
|
Entry entry = choice.entry();
|
||||||
|
int variants = entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
|
||||||
|
for (int vertical = 0; vertical < variants; vertical++) {
|
||||||
|
checkDeadline(deadlineNanos);
|
||||||
|
int targetY = undergroundY - vertical * 20;
|
||||||
|
Structure generated = original;
|
||||||
|
if (entry.family().equals("trial_chambers")) generated = withStartHeight(original, registries, targetY);
|
||||||
|
Predicate<Holder<Biome>> valid = b -> original.biomes().contains(host.structureBiome(b));
|
||||||
|
StructureStart start;
|
||||||
|
if (original instanceof StrongholdStructure) {
|
||||||
|
// Vanilla stronghold piece weights are static mutable state.
|
||||||
|
synchronized (StrongholdStructure.class) {
|
||||||
|
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||||
|
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||||
|
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
|
||||||
|
}
|
||||||
|
if (!start.isValid()) { diagnostic(island, candidate, "native-refused", entry.id()); continue; }
|
||||||
|
// Only buried portal variants need the alternative underground elevations.
|
||||||
|
if (vertical > 0 && entry.family().equals("ruined_portal") && !undergroundPortal(start)) break;
|
||||||
|
start = adjustNativeStart(original, start, entry, island, targetY, ground);
|
||||||
|
String rejection = supportRejection(host, island, entry, start, ground);
|
||||||
|
if (rejection != null) { diagnostic(island, candidate, rejection, entry.id()); continue; }
|
||||||
|
((NativeStructureMetadata23) (Object) start).sanctuary$native23(new NativeStructureMetadata23.Data(island.seed(), seaLevel));
|
||||||
|
return new Accepted(choice.holder(), start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep all pools, processors, aliases, loot, village logic and original registered identity. */
|
||||||
|
public static Structure withStartHeight(Structure original, RegistryAccess registries, int y) {
|
||||||
|
if (!(original instanceof JigsawStructure)) throw new IllegalArgumentException("Expected native jigsaw structure");
|
||||||
|
var ops = RegistryOps.create(JsonOps.INSTANCE, registries);
|
||||||
|
JsonObject json = Structure.DIRECT_CODEC.encodeStart(ops, original).getOrThrow().getAsJsonObject().deepCopy();
|
||||||
|
JsonObject height = new JsonObject(); height.addProperty("absolute", y);
|
||||||
|
json.add("start_height", height);
|
||||||
|
return Structure.DIRECT_CODEC.parse(ops, json).getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureStart adjustNativeStart(Structure original, StructureStart start, Entry entry, ExpansionIsland island,
|
||||||
|
int targetY, GroundSampler ground) {
|
||||||
|
var pieces = start.getPieces();
|
||||||
|
if (entry.family().equals("stronghold") || entry.family().equals("mineshaft") || undergroundPortal(start)) {
|
||||||
|
BoundingBox box = start.getBoundingBox();
|
||||||
|
int dy = targetY - (box.minY() + box.maxY()) / 2;
|
||||||
|
for (StructurePiece piece : pieces) NativeStructurePlacement23.move(piece, dy);
|
||||||
|
} else if (entry.family().equals("igloo")) {
|
||||||
|
// All shaft pieces share the surface entrance's projection. Save final template positions,
|
||||||
|
// rather than the vanilla temporary Y90 restored after each postProcess invocation.
|
||||||
|
var top = (net.minecraft.world.level.levelgen.structure.TemplateStructurePiece) pieces.getLast();
|
||||||
|
var entrance = top.templatePosition().offset(net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate
|
||||||
|
.calculateRelativePosition(top.placeSettings(), new BlockPos(3, 0, 0)));
|
||||||
|
int dy = ground.at(entrance.getX(), entrance.getZ()).top - 90;
|
||||||
|
for (StructurePiece piece : pieces) NativeStructurePlacement23.move(piece, dy);
|
||||||
|
} else if (entry.family().equals("desert_pyramid") || entry.family().equals("jungle_pyramid") || entry.family().equals("swamp_hut")) {
|
||||||
|
// Finalize vanilla's heightPosition before saving starts. Average the full footprint,
|
||||||
|
// avoiding the native dependence on whichever decorating chunk happens to arrive first.
|
||||||
|
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("desert_pyramid") ? lowest : sum / count;
|
||||||
|
int burial = entry.family().equals("desert_pyramid")
|
||||||
|
? (int) Long.remainderUnsigned(mix(island.seed() ^ start.getChunkPos().pack() ^ 0x231D35E47L), 3) : 0;
|
||||||
|
NativeStructurePlacement23.move(piece, height - burial - box.minY());
|
||||||
|
((fr.koka.sanctuary.mixin.ScatteredHeight23Accessor) piece).sanctuary$heightPosition(height);
|
||||||
|
}
|
||||||
|
} else if (entry.family().equals("monument")) {
|
||||||
|
int floor = Integer.MIN_VALUE;
|
||||||
|
BoundingBox box = start.getBoundingBox();
|
||||||
|
for (int z = box.minZ(); z <= box.maxZ(); z += 4) for (int x = box.minX(); x <= box.maxX(); x += 4)
|
||||||
|
floor = Math.max(floor, ExpansionOcean23.column(island, x, z).floorY());
|
||||||
|
int dy = floor + 1 - box.minY();
|
||||||
|
for (StructurePiece piece : pieces) NativeStructurePlacement23.move(piece, dy);
|
||||||
|
} else if (entry.family().equals("shipwreck") || entry.family().equals("ocean_ruin") || entry.family().equals("buried_treasure")) {
|
||||||
|
// Vanilla adjusts these templates during decoration; use the same terrain floor now for valid starts/references.
|
||||||
|
for (StructurePiece piece : pieces) {
|
||||||
|
var box = piece.getBoundingBox();
|
||||||
|
int x = box.getCenter().getX(), z = box.getCenter().getZ();
|
||||||
|
int floor = entry.habitat() == Habitat.OCEAN ? ExpansionOcean23.column(island, x, z).floorY() : ground.at(x, z).top;
|
||||||
|
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.ShipwreckPieces.ShipwreckPiece ship) {
|
||||||
|
// Shipwreck.generate may already change templatePosition without refreshing its bounding box.
|
||||||
|
// Set and certify the final height now instead of translating that stale Y90 box twice.
|
||||||
|
int min = Integer.MAX_VALUE, sum = 0, count = 0;
|
||||||
|
var size = ship.template().getSize();
|
||||||
|
var at = ship.templatePosition();
|
||||||
|
for (int dz = 0; dz < size.getZ(); dz++) for (int dx = 0; dx < size.getX(); dx++) {
|
||||||
|
int height = entry.habitat() == Habitat.OCEAN
|
||||||
|
? ExpansionOcean23.column(island, at.getX() + dx, at.getZ() + dz).floorY() + 1
|
||||||
|
: ground.at(at.getX() + dx, at.getZ() + dz).top + 1;
|
||||||
|
min = Math.min(min, height); sum += height; count++;
|
||||||
|
}
|
||||||
|
int height = count == 0 ? floor + 1 : entry.habitat() == Habitat.SHORE
|
||||||
|
? ship.calculateBeachedPosition(min, net.minecraft.util.RandomSource.create(island.seed() ^ start.getChunkPos().pack())) : sum / count;
|
||||||
|
ship.adjustPositionHeight(height);
|
||||||
|
var actual = ship.template().getBoundingBox(ship.placeSettings(), ship.templatePosition());
|
||||||
|
box.move(0, actual.minY() - box.minY(), 0);
|
||||||
|
} else {
|
||||||
|
NativeStructurePlacement23.move(piece, floor + 1 - box.minY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cloned generators and moved bounding boxes must never escape as a saved registry identity or cached pre-move box.
|
||||||
|
return new StructureStart(original, start.getChunkPos(), start.getReferences(), new PiecesContainer(pieces));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String supportRejection(ExpansionChunkGenerator host, ExpansionIsland island, Entry entry,
|
||||||
|
StructureStart start, GroundSampler ground) {
|
||||||
|
BoundingBox total = start.getBoundingBox();
|
||||||
|
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());
|
||||||
|
if (Math.hypot((double) nearestX - island.centerX(), (double) nearestZ - island.centerZ()) < 56) return "natural-arrival-buffer";
|
||||||
|
}
|
||||||
|
if (total.minY() < ground.level.getMinY() + 3 || total.maxY() > ground.level.getMaxY() - 4) return "outside-height";
|
||||||
|
if (!inside(island, total.minX(), total.minZ(), 3) || !inside(island, total.maxX(), total.minZ(), 3)
|
||||||
|
|| !inside(island, total.minX(), total.maxZ(), 3) || !inside(island, total.maxX(), total.maxZ(), 3)) return "outside-island";
|
||||||
|
// Vanilla reference discovery scans eight chunks around the decorating chunk.
|
||||||
|
if (total.minX() < start.getChunkPos().getMinBlockX() - 128 || total.maxX() > start.getChunkPos().getMaxBlockX() + 128
|
||||||
|
|| total.minZ() < start.getChunkPos().getMinBlockZ() - 128 || total.maxZ() > start.getChunkPos().getMaxBlockZ() + 128) return "outside-native-reference-range";
|
||||||
|
if (host instanceof SanctuaryChunkGenerator city) {
|
||||||
|
if (city.lostCityPlan(island).intersects(total.minX(), total.minZ(), total.maxX(), total.maxZ())) return "occupied-city";
|
||||||
|
if (ground.localCity == null) {
|
||||||
|
var local = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||||
|
local.add(city.lostCityPlan(island), city.undergroundPlan16(island));
|
||||||
|
for (var support : city.citySupports16(island)) local.addViaductSupport(support);
|
||||||
|
var reservation = city.cityReservation16(island);
|
||||||
|
if (reservation != null) local.addReservation(reservation);
|
||||||
|
ground.localCity = local.build();
|
||||||
|
}
|
||||||
|
if (ground.localCity.intersects(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|
||||||
|
|| city.intersectsCityConstruction15(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|
||||||
|
|| city.intersectsCityConstruction16(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|
||||||
|
|| 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(),
|
||||||
|
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean underground = entry.habitat() == Habitat.UNDERGROUND || undergroundPortal(start);
|
||||||
|
boolean aquatic = entry.habitat() == Habitat.OCEAN;
|
||||||
|
int siteMin = Integer.MAX_VALUE, siteMax = Integer.MIN_VALUE;
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
BoundingBox box = piece.getBoundingBox();
|
||||||
|
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE, supported = 0, sampled = 0;
|
||||||
|
boolean buriedPiece = underground || !aquatic && box.maxY() < ground.at(box.getCenter().getX(), box.getCenter().getZ()).top - 6;
|
||||||
|
int step = buriedPiece ? 8 : 4;
|
||||||
|
for (int z : samples(box.minZ(), box.maxZ(), step)) for (int x : samples(box.minX(), box.maxX(), step)) {
|
||||||
|
if (!inside(island, x, z, 3)) return "footprint-outside-island";
|
||||||
|
sampled++;
|
||||||
|
if (aquatic) {
|
||||||
|
var ocean = ExpansionOcean23.column(island, x, z);
|
||||||
|
if (!ocean.water() || ocean.depth() < (entry.family().equals("monument") ? 26 : 5)) return "insufficient-ocean";
|
||||||
|
if (entry.family().equals("monument") && (box.maxY() >= ocean.waterLevel() - 1 || box.minY() - ocean.floorY() > 12)) return "monument-depth-or-foundation";
|
||||||
|
if (!ground.at(x, z).solid(ocean.floorY() - 3)) return "ocean-floor-not-supported";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ground column = ground.at(x, z);
|
||||||
|
if (buriedPiece) {
|
||||||
|
boolean floor = column.solidNear(box.minY() - 1, 6);
|
||||||
|
boolean roof = column.solidNear(box.maxY() + 1, 6);
|
||||||
|
if (column.top >= box.maxY() + 5 && floor && roof) supported++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (column.top < 40 || column.thickness < (entry.family().endsWith("pyramid") ? 18 : 8)) return "unsupported-foundation";
|
||||||
|
if (entry.family().equals("trail_ruins") && !column.solidNear(box.minY() - 1, 4)) return "unsupported-buried-ruin";
|
||||||
|
int maxBurial = entry.family().equals("trail_ruins") || entry.family().endsWith("pyramid") ? 32 : 12;
|
||||||
|
int foundationY = entry.family().equals("mansion") ? total.minY() : box.minY();
|
||||||
|
if (foundationY > column.top + 10 || foundationY < column.top - maxBurial) return "surface-height-mismatch";
|
||||||
|
low = Math.min(low, column.top); high = Math.max(high, column.top);
|
||||||
|
if (high - low > (entry.family().endsWith("pyramid") ? 5 : 9)) return "surface-slope";
|
||||||
|
}
|
||||||
|
if (buriedPiece && supported < Math.ceil(sampled * (entry.family().equals("mineshaft") ? 0.65 : 0.85))) return "cavity-not-enclosed";
|
||||||
|
if (!buriedPiece && !aquatic) {
|
||||||
|
siteMin = Math.min(siteMin, low); siteMax = Math.max(siteMax, high);
|
||||||
|
if (siteMax - siteMin > 20) return "site-slope";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A real-start search for all vanilla keys/tags. No ring-grid fallback and no unactivated island probe. */
|
||||||
|
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> wanted,
|
||||||
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
|
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||||
|
|| generator.rootGeneration() != 23 || !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
|
boolean nativeRequested = wanted.stream().anyMatch(h -> {
|
||||||
|
var e = ExpansionStructureCatalog23.entry(h.unwrapKey().orElseThrow().identifier().toString());
|
||||||
|
return e != null && e.availability() == Availability.OVERWORLD;
|
||||||
|
});
|
||||||
|
boolean sanctuaryRequested = wanted.stream().anyMatch(h -> h.unwrapKey().orElseThrow().identifier().getNamespace().equals("sanctuary"));
|
||||||
|
var candidates = new ArrayList<Candidate>();
|
||||||
|
for (ExpansionIsland island : generator.source().islands()) {
|
||||||
|
if (!nativeRequested || island.generation() != 23 || !island.status().equals("ready")) continue;
|
||||||
|
for (Candidate candidate : candidates(island)) {
|
||||||
|
if (Math.abs((long) candidate.chunk().x() - (origin.getX() >> 4)) <= (long) radius * CELL_CHUNKS
|
||||||
|
&& Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sanctuaryRequested && generator instanceof SanctuaryChunkGenerator sanctuary) addSanctuaryCandidates(sanctuary, candidates);
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
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());
|
||||||
|
if (entry != null && entry.availability() != Availability.OVERWORLD) continue;
|
||||||
|
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||||
|
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 (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);
|
||||||
|
return Pair.of(bestPos, bestHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSanctuaryCandidates(SanctuaryChunkGenerator generator, List<Candidate> positions) {
|
||||||
|
for (ExpansionIsland island : generator.source().islands()) {
|
||||||
|
if (island.generation() != 23 || !island.status().equals("ready")) continue;
|
||||||
|
for (var district : generator.lostCityPlan(island).districts()) addPosition(positions, district.startChunkX(), district.startChunkZ());
|
||||||
|
}
|
||||||
|
generator.heritagePlan19().sites().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||||
|
generator.heritagePlan19().segments().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||||
|
generator.ruinsPlan17().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||||
|
generator.transitPlan21().modules().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
private static void addPosition(List<Candidate> positions, int x, int z) { positions.add(new Candidate(new ChunkPos(x, z), 0)); }
|
||||||
|
private static BlockPos locator(StructureStart start, BlockPos origin) {
|
||||||
|
var entrances = new ArrayList<StructurePiece>();
|
||||||
|
for (StructurePiece piece : start.getPieces()) {
|
||||||
|
if (piece instanceof fr.koka.sanctuary.worldgen.city.LostCityUndergroundPiece20 room
|
||||||
|
&& room.module().kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.ACCESS) entrances.add(piece);
|
||||||
|
if (piece instanceof fr.koka.sanctuary.worldgen.transit.TransitPiece21 tunnel
|
||||||
|
&& tunnel.module().kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) entrances.add(piece);
|
||||||
|
if (piece instanceof fr.koka.sanctuary.worldgen.ruins.RuinsPiece20 room
|
||||||
|
&& room.room().kind() == fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind.ACCESS) entrances.add(piece);
|
||||||
|
}
|
||||||
|
return entrances.stream().map(StructurePiece::getLocatorPosition).min(Comparator.comparingDouble(origin::distSqr))
|
||||||
|
.orElseGet(() -> start.getPieces().getFirst().getLocatorPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean undergroundPortal(StructureStart start) {
|
||||||
|
if (!(start.getPieces().getFirst() instanceof net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece portal)) return false;
|
||||||
|
var placement = ((fr.koka.sanctuary.mixin.RuinedPortal23Accessor) portal).sanctuary$verticalPlacement();
|
||||||
|
return placement == net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece.VerticalPlacement.UNDERGROUND
|
||||||
|
|| placement == net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece.VerticalPlacement.IN_MOUNTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sizeEligible(Entry entry, ExpansionIsland island) {
|
||||||
|
return switch (entry.family()) {
|
||||||
|
case "mansion", "trial_chambers", "stronghold" -> island.diameter() >= 256;
|
||||||
|
case "village", "monument" -> island.diameter() >= 256;
|
||||||
|
default -> true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static int weight(String family) {
|
||||||
|
return switch (family) { case "camp" -> 6; case "ruined_portal", "mineshaft", "ocean_ruin" -> 3;
|
||||||
|
case "village", "shipwreck", "trail_ruins", "buried_treasure" -> 2; default -> 1; };
|
||||||
|
}
|
||||||
|
private static int[] samples(int low, int high, int step) {
|
||||||
|
var values = new java.util.TreeSet<Integer>(); values.add(low); values.add(high); values.add((low + high) / 2);
|
||||||
|
for (int value = low; value <= high; value += step) values.add(value);
|
||||||
|
return values.stream().mapToInt(Integer::intValue).toArray();
|
||||||
|
}
|
||||||
|
private static boolean inside(ExpansionIsland island, int x, int z, int margin) {
|
||||||
|
return Math.hypot((double) x - island.centerX(), (double) z - island.centerZ()) < island.terrainRadius() - margin;
|
||||||
|
}
|
||||||
|
private static double horizontalDistanceSquared(BlockPos origin, int x, int z) { double dx = (double) x - origin.getX(), dz = (double) z - origin.getZ(); return dx * dx + dz * dz; }
|
||||||
|
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||||
|
private static void checkDeadline(long deadline) { if (System.nanoTime() > deadline) throw new IllegalArgumentException("search_timeout"); }
|
||||||
|
private static void diagnostic(ExpansionIsland island, Candidate candidate, String reason, String id) {
|
||||||
|
if (DIAGNOSTICS) SanctuaryMod.LOGGER.info("Native23 {} seed {} chunk {} {} {}", island.id(), island.seed(), candidate.chunk(), reason, id);
|
||||||
|
}
|
||||||
|
private record Choice(Entry entry, Holder<Structure> holder, double score) {}
|
||||||
|
private static final class Ground {
|
||||||
|
final NoiseColumn column; final int top; final int thickness;
|
||||||
|
Ground(NoiseColumn column, int min, int max) {
|
||||||
|
this.column = column; int y = max;
|
||||||
|
while (y >= min && !column.getBlock(y).isSolid()) y--;
|
||||||
|
top = y;
|
||||||
|
while (y >= min && top - y < 24 && solid(y)) y--;
|
||||||
|
thickness = top - y;
|
||||||
|
}
|
||||||
|
boolean solid(int y) { var block = column.getBlock(y); return block.isSolid() && block.getFluidState().isEmpty(); }
|
||||||
|
boolean solidNear(int y, int tolerance) { for (int dy = -tolerance; dy <= tolerance; dy++) if (solid(y + dy)) return true; return false; }
|
||||||
|
}
|
||||||
|
private static final class GroundSampler {
|
||||||
|
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
columns.put(key, result); return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static ExpansionBiomeSource isolatedSource(ExpansionChunkGenerator host, ExpansionIsland island, RegistryAccess registries) {
|
||||||
|
ExpansionBiomeSource isolated;
|
||||||
|
if (host.source() instanceof fr.koka.sanctuary.worldgen.SanctuaryBiomeSource source) {
|
||||||
|
var copy = source.copy();
|
||||||
|
if (host instanceof SanctuaryChunkGenerator sanctuary) copy.setCityPlans(Map.of(island.id(), sanctuary.lostCityPlan(island)));
|
||||||
|
isolated = copy;
|
||||||
|
} else {
|
||||||
|
List<Holder<Biome>> biomes = registries.lookupOrThrow(Registries.BIOME).listElements().map(h -> (Holder<Biome>) h).toList();
|
||||||
|
isolated = new ExpansionBiomeSource(biomes);
|
||||||
|
}
|
||||||
|
isolated.setIslands(List.of(island));
|
||||||
|
return isolated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
|
||||||
|
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
|
||||||
|
}
|
||||||
|
@Override public int rootGeneration() { return 23; }
|
||||||
|
@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) {
|
||||||
|
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) {
|
||||||
|
return island.origin() ? host.getBaseColumn(x, z, level, random) : super.getBaseColumn(x, z, level, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
/** Optional metadata on native starts, absent on all historical and ordinary Minecraft starts. */
|
||||||
|
public interface NativeStructureMetadata23 {
|
||||||
|
record Data(long islandSeed, int seaLevel) {}
|
||||||
|
Data sanctuary$native23();
|
||||||
|
void sanctuary$native23(Data data);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.mixin.MonumentChildren23Accessor;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
|
||||||
|
/** Scoped native placement parameters; never change the sea level of another dimension or start. */
|
||||||
|
public final class NativeStructurePlacement23 {
|
||||||
|
private static final ThreadLocal<Integer> SEA_LEVEL = new ThreadLocal<>();
|
||||||
|
private NativeStructurePlacement23() {}
|
||||||
|
public static Integer seaLevel() { return SEA_LEVEL.get(); }
|
||||||
|
public static Integer enter(int seaLevel) { Integer previous = SEA_LEVEL.get(); SEA_LEVEL.set(seaLevel); return previous; }
|
||||||
|
public static void restore(Integer previous) { if (previous == null) SEA_LEVEL.remove(); else SEA_LEVEL.set(previous); }
|
||||||
|
public static void move(StructurePiece piece, int dy) {
|
||||||
|
if (piece instanceof OceanMonumentPieces.MonumentBuilding monument) {
|
||||||
|
for (StructurePiece child : ((MonumentChildren23Accessor) monument).sanctuary$children()) child.move(0, dy, 0);
|
||||||
|
}
|
||||||
|
piece.move(0, dy, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package fr.koka.sanctuary.knowledge.structures;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import java.util.List;
|
||||||
|
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.commands.SharedSuggestionProvider;
|
||||||
|
import net.minecraft.commands.arguments.IdentifierArgument;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
/** Operator-only aggregate diagnostics, with no coordinates or player discovery sent to the Blocodex. */
|
||||||
|
public final class StructureCensusCommands {
|
||||||
|
private static final String TEXT = "sanctuary.structures.census.";
|
||||||
|
private StructureCensusCommands() {}
|
||||||
|
public static void register() {
|
||||||
|
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) -> {
|
||||||
|
var structures = Commands.literal("structures").executes(context -> report(context.getSource(), null))
|
||||||
|
.then(Commands.literal("island").then(Commands.argument("id", StringArgumentType.word())
|
||||||
|
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
|
||||||
|
StructureCensusService.snapshot(context.getSource().getServer()).regions().stream()
|
||||||
|
.map(StructureCensusService.Region::island).distinct(), builder))
|
||||||
|
.executes(context -> report(context.getSource(), StringArgumentType.getString(context, "id")))))
|
||||||
|
.then(Commands.literal("dimension").then(Commands.argument("id", IdentifierArgument.id())
|
||||||
|
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
|
||||||
|
StructureCensusService.snapshot(context.getSource().getServer()).dimensions().stream()
|
||||||
|
.map(StructureCensusService.Dimension::dimension), builder))
|
||||||
|
.executes(context -> dimension(context.getSource(), IdentifierArgument.getId(context, "id").toString()))));
|
||||||
|
dispatcher.register(Commands.literal("sanctuary").requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||||
|
.then(Commands.literal("census").then(structures)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private static int report(CommandSourceStack source, String island) {
|
||||||
|
var snapshot = StructureCensusService.snapshot(source.getServer());
|
||||||
|
if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; }
|
||||||
|
var evidence = snapshot.evidence();
|
||||||
|
long nativeStarts = evidence.sites().keySet().stream().filter(key -> !key.feature()).count();
|
||||||
|
send(source, "summary", nativeStarts, evidence.sites().size() - nativeStarts, evidence.chunks().size());
|
||||||
|
List<StructureCensusService.Region> regions = snapshot.regions().stream()
|
||||||
|
.filter(region -> island == null || region.island().equals(island)).toList();
|
||||||
|
if (island != null && regions.isEmpty()) { source.sendFailure(Component.translatable(TEXT + "unknown_island", island)); return 0; }
|
||||||
|
for (var region : regions.stream().limit(16).toList()) {
|
||||||
|
var coverage = region.coverage();
|
||||||
|
send(source, "region", region.dimension(), region.island(), region.status(), coverage.startsChunks(), coverage.expectedChunks(),
|
||||||
|
coverage.featurePassChunks(), coverage.expectedChunks(), coverage.fullChunks(), coverage.expectedChunks());
|
||||||
|
if (island != null) region.families().forEach((family, count) -> send(source, "family", family, count.accepted(), count.generated(),
|
||||||
|
Component.translatable(TEXT + "status." + count.presence().name().toLowerCase(java.util.Locale.ROOT))));
|
||||||
|
}
|
||||||
|
if (regions.size() > 16) send(source, "limited", 16, regions.size());
|
||||||
|
if (island == null) snapshot.dimensions().forEach(dimension -> send(source, "dimension", dimension.dimension(), dimension.observedChunks()));
|
||||||
|
send(source, "help");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
private static int dimension(CommandSourceStack source, String id) {
|
||||||
|
var snapshot = StructureCensusService.snapshot(source.getServer());
|
||||||
|
if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; }
|
||||||
|
var selected = snapshot.dimensions().stream().filter(value -> value.dimension().equals(id)).findFirst();
|
||||||
|
if (selected.isEmpty()) { source.sendFailure(Component.translatable(TEXT + "unknown_dimension", id)); return 0; }
|
||||||
|
var dimension = selected.get(); send(source, "dimension", dimension.dimension(), dimension.observedChunks());
|
||||||
|
dimension.families().entrySet().stream().sorted(java.util.Map.Entry.comparingByKey()).forEach(entry -> {
|
||||||
|
var count = entry.getValue();
|
||||||
|
send(source, "family", entry.getKey(), count.accepted(), count.generated(),
|
||||||
|
Component.translatable(TEXT + "status." + count.presence().name().toLowerCase(java.util.Locale.ROOT)));
|
||||||
|
});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
private static void send(CommandSourceStack source, String key, Object... arguments) {
|
||||||
|
source.sendSuccess(() -> Component.translatable(TEXT + key, arguments), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
package fr.koka.sanctuary.knowledge.structures;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/** Observed native evidence, independent of loaded chunks, player visits and map references. */
|
||||||
|
public final class StructureCensusMemory {
|
||||||
|
public static final int STARTS = 1, FEATURES = 2, FULL = 4;
|
||||||
|
public static final int MAX_CHUNKS = 500_000, MAX_SITES = 100_000;
|
||||||
|
public static final String MONSTER_ROOM = "minecraft:monster_room";
|
||||||
|
private final Map<ChunkKey, Integer> chunks = new HashMap<>();
|
||||||
|
private final Map<SiteKey, Site> sites = new HashMap<>();
|
||||||
|
|
||||||
|
public StructureCensusMemory() {}
|
||||||
|
public StructureCensusMemory(Snapshot saved) {
|
||||||
|
chunks.putAll(saved.chunks()); sites.putAll(saved.sites());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Repeated loads merge the same departure once. Empty starts are evidence only at this chunk. */
|
||||||
|
public boolean observe(ChunkKey chunk, int flags, List<Site> starts) {
|
||||||
|
validateFlags(flags);
|
||||||
|
for (var site : starts) {
|
||||||
|
if (!site.key().chunk().equals(chunk) || site.key().feature())
|
||||||
|
throw new IllegalArgumentException("Start evidence belongs to a different chunk");
|
||||||
|
}
|
||||||
|
int before = chunks.getOrDefault(chunk, 0), after = before | flags;
|
||||||
|
if (chunks.size() >= MAX_CHUNKS && !chunks.containsKey(chunk)) throw new IllegalStateException("Structure census chunk limit");
|
||||||
|
boolean changed = before != after;
|
||||||
|
chunks.put(chunk, after);
|
||||||
|
// This is a historical census: later missing metadata or dismantled ruins cannot
|
||||||
|
// erase an accepted occurrence or create a fresh absence bonus for that family.
|
||||||
|
for (var site : starts) changed |= observeSite(site);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean observeFeature(Site site) {
|
||||||
|
if (!site.key().feature()) throw new IllegalArgumentException("Not a feature observation");
|
||||||
|
return observeSite(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean observeSite(Site site) {
|
||||||
|
var previous = sites.get(site.key());
|
||||||
|
if (previous != null) {
|
||||||
|
// The native metadata on a subsequent load is authoritative for its saved geometry.
|
||||||
|
if (previous.equals(site)) return false;
|
||||||
|
} else if (sites.size() >= MAX_SITES) throw new IllegalStateException("Structure census site limit");
|
||||||
|
sites.put(site.key(), site);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot snapshot() { return new Snapshot(chunks, sites); }
|
||||||
|
|
||||||
|
public record ChunkKey(String dimension, int x, int z) implements Comparable<ChunkKey> {
|
||||||
|
public ChunkKey { identifier(dimension); coordinate(x, 2_000_000); coordinate(z, 2_000_000); }
|
||||||
|
@Override public int compareTo(ChunkKey other) {
|
||||||
|
int comparison = dimension.compareTo(other.dimension);
|
||||||
|
if (comparison == 0) comparison = Integer.compare(x, other.x);
|
||||||
|
return comparison == 0 ? Integer.compare(z, other.z) : comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Native x/z are the departure chunk, with y=0. Feature x/y/z are an exact placed origin. */
|
||||||
|
public record SiteKey(String dimension, String id, int x, int y, int z, boolean feature) implements Comparable<SiteKey> {
|
||||||
|
public SiteKey {
|
||||||
|
identifier(dimension); identifier(id);
|
||||||
|
coordinate(x, feature ? 32_000_000 : 2_000_000); coordinate(z, feature ? 32_000_000 : 2_000_000);
|
||||||
|
coordinate(y, 4096);
|
||||||
|
if (!feature && y != 0) throw new IllegalArgumentException("Native structure key uses its departure chunk");
|
||||||
|
}
|
||||||
|
public ChunkKey chunk() { return new ChunkKey(dimension, feature ? Math.floorDiv(x, 16) : x, feature ? Math.floorDiv(z, 16) : z); }
|
||||||
|
@Override public int compareTo(SiteKey other) {
|
||||||
|
int comparison = dimension.compareTo(other.dimension);
|
||||||
|
if (comparison == 0) comparison = id.compareTo(other.id);
|
||||||
|
if (comparison == 0) comparison = Boolean.compare(feature, other.feature);
|
||||||
|
if (comparison == 0) comparison = Integer.compare(x, other.x);
|
||||||
|
if (comparison == 0) comparison = Integer.compare(y, other.y);
|
||||||
|
return comparison == 0 ? Integer.compare(z, other.z) : comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Bounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
|
public Bounds {
|
||||||
|
coordinate(minX, 32_000_000); coordinate(maxX, 32_000_000);
|
||||||
|
coordinate(minZ, 32_000_000); coordinate(maxZ, 32_000_000);
|
||||||
|
coordinate(minY, 4096); coordinate(maxY, 4096);
|
||||||
|
if (minX > maxX || minY > maxY || minZ > maxZ || (long) maxX - minX > 4096 || (long) maxZ - minZ > 4096)
|
||||||
|
throw new IllegalArgumentException("Invalid structure evidence bounds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Site(SiteKey key, List<Bounds> pieces) {
|
||||||
|
public Site {
|
||||||
|
Objects.requireNonNull(key); pieces = List.copyOf(pieces);
|
||||||
|
if (pieces.isEmpty() || pieces.size() > 4096) throw new IllegalArgumentException("Invalid structure piece evidence");
|
||||||
|
if (key.feature() && (!key.id().equals(MONSTER_ROOM) || pieces.size() != 1))
|
||||||
|
throw new IllegalArgumentException("Unsupported placed feature evidence");
|
||||||
|
long chunkChecks = 0;
|
||||||
|
for (var piece : pieces) {
|
||||||
|
chunkChecks += (long) (Math.floorDiv(piece.maxX(), 16) - Math.floorDiv(piece.minX(), 16) + 1)
|
||||||
|
* (Math.floorDiv(piece.maxZ(), 16) - Math.floorDiv(piece.minZ(), 16) + 1);
|
||||||
|
if (chunkChecks > 65_536) throw new IllegalArgumentException("Structure evidence footprint exceeds its inspection budget");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Snapshot(Map<ChunkKey, Integer> chunks, Map<SiteKey, Site> sites) {
|
||||||
|
public Snapshot {
|
||||||
|
if (chunks.size() > MAX_CHUNKS || sites.size() > MAX_SITES) throw new IllegalArgumentException("Structure census exceeds limits");
|
||||||
|
chunks.forEach((key, flags) -> { Objects.requireNonNull(key); validateFlags(flags); });
|
||||||
|
sites.forEach((key, site) -> { if (!key.equals(site.key())) throw new IllegalArgumentException("Mismatched structure evidence key"); });
|
||||||
|
chunks = Collections.unmodifiableMap(new TreeMap<>(chunks));
|
||||||
|
sites = Collections.unmodifiableMap(new TreeMap<>(sites));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Coverage coverage(List<ChunkKey> scope) {
|
||||||
|
int starts = 0, features = 0, full = 0;
|
||||||
|
for (var key : scope) {
|
||||||
|
int flags = chunks.getOrDefault(key, 0);
|
||||||
|
if ((flags & STARTS) != 0) starts++;
|
||||||
|
if ((flags & FEATURES) != 0) features++;
|
||||||
|
if ((flags & FULL) != 0) full++;
|
||||||
|
}
|
||||||
|
return new Coverage(scope.size(), starts, features, full);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A FULL departure alone is insufficient when pieces continue into unknown neighbours. */
|
||||||
|
public boolean generated(Site site) {
|
||||||
|
if (site.key().feature()) return true; // Recorded only after the native feature returned success.
|
||||||
|
for (var box : site.pieces()) for (int x = Math.floorDiv(box.minX(), 16); x <= Math.floorDiv(box.maxX(), 16); x++)
|
||||||
|
for (int z = Math.floorDiv(box.minZ(), 16); z <= Math.floorDiv(box.maxZ(), 16); z++)
|
||||||
|
if ((chunks.getOrDefault(new ChunkKey(site.key().dimension(), x, z), 0) & (FEATURES | FULL)) == 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Site> inChunks(java.util.Set<ChunkKey> scope) {
|
||||||
|
var found = new ArrayList<Site>();
|
||||||
|
for (var site : sites.values()) if (scope.contains(site.key().chunk())) found.add(site);
|
||||||
|
return List.copyOf(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Coverage(int expectedChunks, int startsChunks, int featurePassChunks, int fullChunks) {
|
||||||
|
public boolean startsComplete() { return expectedChunks > 0 && startsChunks == expectedChunks; }
|
||||||
|
public boolean featuresComplete() { return expectedChunks > 0 && featurePassChunks == expectedChunks; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateFlags(int flags) {
|
||||||
|
if (flags <= 0 || (flags & ~(STARTS | FEATURES | FULL)) != 0) throw new IllegalArgumentException("Invalid structure census coverage flags");
|
||||||
|
}
|
||||||
|
private static void identifier(String value) {
|
||||||
|
if (value == null || value.length() > 256 || !value.matches("[a-z0-9_.-]+:[a-z0-9_/.-]+"))
|
||||||
|
throw new IllegalArgumentException("Invalid registry identifier in structure census");
|
||||||
|
}
|
||||||
|
private static void coordinate(int value, int max) {
|
||||||
|
if (Math.abs((long) value) > max) throw new IllegalArgumentException("Structure evidence coordinate out of range");
|
||||||
|
}
|
||||||
|
}
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
package fr.koka.sanctuary.knowledge.structures;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructureCatalog23;
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
||||||
|
/** Passive native evidence. No tickets, full-world scans, player discovery or reference-counter writes. */
|
||||||
|
public final class StructureCensusService {
|
||||||
|
private static final Map<MinecraftServer, Session> SERVERS = new IdentityHashMap<>();
|
||||||
|
private static final int SAVE_INTERVAL = 600;
|
||||||
|
private StructureCensusService() {}
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
ServerChunkEvents.CHUNK_LOAD.register((level, chunk, newlyGenerated) -> observeChunk(level, chunk, StructureCensusMemory.STARTS | StructureCensusMemory.FULL));
|
||||||
|
ServerTickEvents.END_SERVER_TICK.register(server -> { if (server.getTickCount() % SAVE_INTERVAL == 0) flush(server); });
|
||||||
|
ServerLifecycleEvents.BEFORE_SAVE.register((server, flush, force) -> flush(server));
|
||||||
|
ServerLifecycleEvents.SERVER_STOPPING.register(StructureCensusService::flush);
|
||||||
|
ServerLifecycleEvents.SERVER_STOPPED.register(SERVERS::remove);
|
||||||
|
StructureCensusCommands.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void observeStarts(ServerLevel level, ChunkAccess chunk) { observeChunk(level, chunk, StructureCensusMemory.STARTS); }
|
||||||
|
/** Only a successfully completed native FEATURES pass certifies feature absences. Loading FULL cannot reconstruct them. */
|
||||||
|
public static void observeFeatures(ServerLevel level, ChunkAccess chunk) {
|
||||||
|
observeChunk(level, chunk, StructureCensusMemory.STARTS | StructureCensusMemory.FEATURES);
|
||||||
|
}
|
||||||
|
private static void observeChunk(ServerLevel level, ChunkAccess chunk, int flags) {
|
||||||
|
try {
|
||||||
|
String dimension = level.dimension().identifier().toString();
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
var observations = new ArrayList<Site>();
|
||||||
|
for (var entry : chunk.getAllStarts().entrySet()) {
|
||||||
|
var start = entry.getValue(); if (start == null || !start.isValid()) continue;
|
||||||
|
var id = registry.getKey(entry.getKey()); if (id == null) continue;
|
||||||
|
var pieces = new ArrayList<Bounds>();
|
||||||
|
for (var piece : start.getPieces()) {
|
||||||
|
var b = piece.getBoundingBox();
|
||||||
|
pieces.add(new Bounds(b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ()));
|
||||||
|
}
|
||||||
|
observations.add(new Site(new SiteKey(dimension, id.toString(), start.getChunkPos().x(), 0, start.getChunkPos().z(), false), pieces));
|
||||||
|
}
|
||||||
|
var key = new ChunkKey(dimension, chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
var snapshot = List.copyOf(observations);
|
||||||
|
onServer(level.getServer(), session -> session.dirty |= session.memory.observe(key, flags, snapshot));
|
||||||
|
} catch (RuntimeException error) { reportFailure(level.getServer(), error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void observeMonsterRoom(WorldGenLevel world, BlockPos origin) {
|
||||||
|
var level = world.getLevel();
|
||||||
|
try {
|
||||||
|
var key = new SiteKey(level.dimension().identifier().toString(), StructureCensusMemory.MONSTER_ROOM,
|
||||||
|
origin.getX(), origin.getY(), origin.getZ(), true);
|
||||||
|
// This is the placed feature origin, not an inferred exact room footprint.
|
||||||
|
var site = new Site(key, List.of(new Bounds(origin.getX(), origin.getY(), origin.getZ(), origin.getX(), origin.getY(), origin.getZ())));
|
||||||
|
onServer(level.getServer(), session -> session.dirty |= session.memory.observeFeature(site));
|
||||||
|
} catch (RuntimeException error) { reportFailure(level.getServer(), error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable source for generation decisions. Unknown coverage never receives an absence bonus. */
|
||||||
|
public static List<String> priorities(ServerLevel level) {
|
||||||
|
assertServerThread(level.getServer());
|
||||||
|
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|
||||||
|
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) || generator.rootGeneration() != 23)
|
||||||
|
return List.of();
|
||||||
|
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An absence belongs to a completely inspected region, never to the infinite dimension.
|
||||||
|
* Unknown neighbouring regions do not cancel that observation; any known presence does. */
|
||||||
|
public static List<String> confirmedPriorities(View view, String dimension) {
|
||||||
|
if (!view.available()) return List.of();
|
||||||
|
var absent = new TreeSet<String>();
|
||||||
|
for (var region : view.regions()) if (region.dimension().equals(dimension) && region.status().equals("ready")
|
||||||
|
&& region.coverage().startsComplete()) {
|
||||||
|
region.families().forEach((family, count) -> { if (count.presence() == Presence.ABSENT) absent.add(family); });
|
||||||
|
}
|
||||||
|
absent.retainAll(overworldFamilies());
|
||||||
|
for (var site : view.evidence().sites().values()) if (site.key().dimension().equals(dimension)) absent.remove(family(site.key().id()));
|
||||||
|
return List.copyOf(absent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static View snapshot(MinecraftServer server) {
|
||||||
|
assertServerThread(server);
|
||||||
|
var session = current(server);
|
||||||
|
var evidence = session.memory.snapshot();
|
||||||
|
var regions = new ArrayList<Region>();
|
||||||
|
var dimensions = new ArrayList<Dimension>();
|
||||||
|
for (var level : server.getAllLevels()) {
|
||||||
|
String dimension = level.dimension().identifier().toString();
|
||||||
|
var observed = evidence.sites().values().stream().filter(site -> site.key().dimension().equals(dimension)).toList();
|
||||||
|
int observedChunks = (int) evidence.chunks().keySet().stream().filter(key -> key.dimension().equals(dimension)).count();
|
||||||
|
dimensions.add(new Dimension(dimension, observedChunks, counts(evidence, observed, new Coverage(0, 0, 0, 0),
|
||||||
|
session.failed || !level.structureManager().shouldGenerateStructures(), dimension, null)));
|
||||||
|
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)) continue;
|
||||||
|
for (var island : generator.source().islands()) {
|
||||||
|
var scope = scope(dimension, island); var coverage = evidence.coverage(scope);
|
||||||
|
var sites = evidence.inChunks(Set.copyOf(scope));
|
||||||
|
regions.add(new Region(dimension, island.id(), island.generation(), island.status(), coverage,
|
||||||
|
counts(evidence, sites, coverage, session.failed || !level.structureManager().shouldGenerateStructures(), dimension, island)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new View(!session.failed, session.dirty, evidence, regions, dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, FamilyCount> counts(StructureCensusMemory.Snapshot evidence, List<Site> sites, Coverage coverage,
|
||||||
|
boolean unknown, String dimension, ExpansionIsland island) {
|
||||||
|
var result = new TreeMap<String, FamilyCount>();
|
||||||
|
var families = new TreeSet<>(ExpansionStructureCatalog23.entries().stream().map(ExpansionStructureCatalog23.Entry::family).toList());
|
||||||
|
families.add("monster_room");
|
||||||
|
sites.forEach(site -> families.add(family(site.key().id())));
|
||||||
|
for (String family : families) {
|
||||||
|
int accepted = 0, generated = 0;
|
||||||
|
for (var site : sites) if (family(site.key().id()).equals(family)) {
|
||||||
|
accepted++; if (evidence.generated(site)) generated++;
|
||||||
|
}
|
||||||
|
boolean complete = family.equals("monster_room") ? coverage.featuresComplete() : coverage.startsComplete();
|
||||||
|
Presence incompatible = compatibility(family, dimension, island);
|
||||||
|
Presence presence = unknown ? Presence.UNKNOWN : accepted > 0 ? Presence.PRESENT : incompatible != null ? incompatible
|
||||||
|
: complete ? Presence.ABSENT : Presence.UNKNOWN;
|
||||||
|
result.put(family, new FamilyCount(accepted, generated, presence));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Presence compatibility(String family, String dimension, ExpansionIsland island) {
|
||||||
|
var entries = ExpansionStructureCatalog23.entries().stream().filter(entry -> entry.family().equals(family)).toList();
|
||||||
|
if (entries.isEmpty()) return null;
|
||||||
|
var matching = entries.stream().filter(entry -> switch (entry.availability()) {
|
||||||
|
case OVERWORLD -> dimension.equals("minecraft:overworld");
|
||||||
|
case NETHER_ONLY -> dimension.equals("minecraft:the_nether");
|
||||||
|
case END_ONLY -> dimension.equals("minecraft:the_end");
|
||||||
|
case CAVERNS_ONLY -> false; // That dimension is not implemented by alpha23.1.
|
||||||
|
}).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.relief().equals("ocean")
|
||||||
|
&& matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> overworldFamilies() {
|
||||||
|
return ExpansionStructureCatalog23.entries().stream()
|
||||||
|
.filter(entry -> entry.availability() == ExpansionStructureCatalog23.Availability.OVERWORLD)
|
||||||
|
.map(ExpansionStructureCatalog23.Entry::family).distinct().sorted().toList();
|
||||||
|
}
|
||||||
|
public static String family(String id) {
|
||||||
|
if (id.equals(StructureCensusMemory.MONSTER_ROOM)) return "monster_room";
|
||||||
|
return ExpansionStructureCatalog23.entries().stream().filter(entry -> entry.id().equals(id))
|
||||||
|
.map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finite ownership is derived from the immutable expansion journal descriptors,
|
||||||
|
* rather than duplicated in the sidecar or inferred from currently loaded chunks. */
|
||||||
|
public static List<ChunkKey> scope(String dimension, ExpansionIsland island) {
|
||||||
|
int radius = island.terrainRadius() + ExpansionIsland.DECORATION_MARGIN;
|
||||||
|
var result = new ArrayList<ChunkKey>();
|
||||||
|
for (int x = Math.floorDiv(island.centerX() - radius, 16); x <= Math.floorDiv(island.centerX() + radius, 16); x++)
|
||||||
|
for (int z = Math.floorDiv(island.centerZ() - radius, 16); z <= Math.floorDiv(island.centerZ() + radius, 16); z++)
|
||||||
|
if (island.ownsChunk(x, z)) result.add(new ChunkKey(dimension, x, z));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onServer(MinecraftServer server, java.util.function.Consumer<Session> observation) {
|
||||||
|
Runnable task = () -> {
|
||||||
|
var session = current(server); if (session.failed) return;
|
||||||
|
try { observation.accept(session); } catch (RuntimeException error) { fail(session, error); }
|
||||||
|
};
|
||||||
|
if (server.isSameThread()) task.run(); else server.execute(task);
|
||||||
|
}
|
||||||
|
private static void reportFailure(MinecraftServer server, Exception error) {
|
||||||
|
Runnable task = () -> fail(current(server), error);
|
||||||
|
if (server.isSameThread()) task.run(); else server.execute(task);
|
||||||
|
}
|
||||||
|
public static void flush(MinecraftServer server) {
|
||||||
|
assertServerThread(server); var session = SERVERS.get(server);
|
||||||
|
if (session == null || session.failed || !session.dirty) return;
|
||||||
|
try { session.store.save(session.memory.snapshot()); session.dirty = false; }
|
||||||
|
catch (IOException | RuntimeException error) { fail(session, error); }
|
||||||
|
}
|
||||||
|
private static Session current(MinecraftServer server) {
|
||||||
|
assertServerThread(server);
|
||||||
|
var found = SERVERS.get(server); if (found != null) return found;
|
||||||
|
var store = new StructureCensusStore(server.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-structures"),
|
||||||
|
server.getWorldGenSettings().options().seed());
|
||||||
|
var session = new Session(store); SERVERS.put(server, session);
|
||||||
|
try { session.memory = new StructureCensusMemory(store.load()); }
|
||||||
|
catch (IOException | RuntimeException error) { fail(session, error); }
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
private static void fail(Session session, Exception error) {
|
||||||
|
if (!session.failed) SanctuaryMod.LOGGER.error("Structure census unavailable; priorities remain neutral and existing evidence is preserved at {}", session.store.path(), error);
|
||||||
|
session.failed = true;
|
||||||
|
}
|
||||||
|
private static void assertServerThread(MinecraftServer server) {
|
||||||
|
if (!server.isSameThread()) throw new IllegalStateException("Structure census must run on the server thread");
|
||||||
|
}
|
||||||
|
private static final class Session {
|
||||||
|
final StructureCensusStore store; StructureCensusMemory memory = new StructureCensusMemory(); boolean failed, dirty;
|
||||||
|
Session(StructureCensusStore store) { this.store = store; }
|
||||||
|
}
|
||||||
|
public enum Presence { PRESENT, ABSENT, UNKNOWN, INCOMPATIBLE, OTHER_DIMENSION }
|
||||||
|
public record FamilyCount(int accepted, int generated, Presence presence) {}
|
||||||
|
public record Region(String dimension, String island, int generation, String status, Coverage coverage, Map<String, FamilyCount> families) {
|
||||||
|
public Region { families = Map.copyOf(families); }
|
||||||
|
}
|
||||||
|
public record Dimension(String dimension, int observedChunks, Map<String, FamilyCount> families) {
|
||||||
|
public Dimension { families = Map.copyOf(families); }
|
||||||
|
}
|
||||||
|
public record View(boolean available, boolean unsaved, StructureCensusMemory.Snapshot evidence, List<Region> regions, List<Dimension> dimensions) {
|
||||||
|
public View { regions = List.copyOf(regions); dimensions = List.copyOf(dimensions); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
package fr.koka.sanctuary.knowledge.structures;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.Strictness;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonToken;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory.*;
|
||||||
|
|
||||||
|
/** Separate derived index; native chunk metadata and the immutable expansion journal are not rewritten. */
|
||||||
|
public final class StructureCensusStore {
|
||||||
|
private static final int MAX_BYTES = 64 * 1024 * 1024;
|
||||||
|
private static final Gson JSON = new Gson();
|
||||||
|
private final Path directory, file;
|
||||||
|
private final long worldSeed;
|
||||||
|
private byte[] previousBytes;
|
||||||
|
private Snapshot previous;
|
||||||
|
private boolean loaded, failed;
|
||||||
|
|
||||||
|
public StructureCensusStore(Path directory, long worldSeed) {
|
||||||
|
this.directory = directory.toAbsolutePath().normalize(); this.worldSeed = worldSeed;
|
||||||
|
file = this.directory.resolve("index.json");
|
||||||
|
}
|
||||||
|
public Path path() { return file; }
|
||||||
|
public synchronized Snapshot load() throws IOException {
|
||||||
|
if (failed) throw new IOException("Structure census failed earlier; reload after repair");
|
||||||
|
try {
|
||||||
|
checkDirectory(); previousBytes = readBytes();
|
||||||
|
previous = previousBytes == null ? new Snapshot(Map.of(), Map.of()) : decode(previousBytes);
|
||||||
|
loaded = true; return previous;
|
||||||
|
} catch (IOException | RuntimeException error) { failed = true; throw io("Could not load structure census", error); }
|
||||||
|
}
|
||||||
|
public synchronized void save(Snapshot snapshot) throws IOException {
|
||||||
|
if (!loaded || failed) throw new IOException("Structure census must load successfully before saving");
|
||||||
|
Path temporary = null;
|
||||||
|
try {
|
||||||
|
checkDirectory();
|
||||||
|
if (!Arrays.equals(previousBytes, readBytes())) throw new IOException("Structure census changed outside this store");
|
||||||
|
if (previous.equals(snapshot)) return;
|
||||||
|
byte[] bytes = encode(snapshot);
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
temporary = Files.createTempFile(directory, ".structures-", ".tmp");
|
||||||
|
try (var channel = FileChannel.open(temporary, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
var buffer = ByteBuffer.wrap(bytes); while (buffer.hasRemaining()) channel.write(buffer); channel.force(true);
|
||||||
|
}
|
||||||
|
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); temporary = null;
|
||||||
|
if (!file.getFileSystem().getSeparator().equals("\\")) {
|
||||||
|
try (var channel = FileChannel.open(directory, StandardOpenOption.READ)) { channel.force(true); }
|
||||||
|
}
|
||||||
|
byte[] verified = readBytes();
|
||||||
|
if (!Arrays.equals(bytes, verified) || !decode(verified).equals(snapshot)) throw new IOException("Structure census read-back differs");
|
||||||
|
previousBytes = verified; previous = snapshot;
|
||||||
|
} catch (IOException | RuntimeException error) { failed = true; throw io("Could not save structure census", error); }
|
||||||
|
finally { if (temporary != null) try { Files.deleteIfExists(temporary); } catch (IOException ignored) { } }
|
||||||
|
}
|
||||||
|
private void checkDirectory() throws IOException {
|
||||||
|
if (!Files.notExists(directory, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS))
|
||||||
|
throw new IOException("Structure census path is not a regular directory");
|
||||||
|
}
|
||||||
|
private byte[] readBytes() throws IOException {
|
||||||
|
if (Files.notExists(file, LinkOption.NOFOLLOW_LINKS)) return null;
|
||||||
|
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.size(file) > MAX_BYTES)
|
||||||
|
throw new IOException("Structure census file is invalid or too large");
|
||||||
|
try (var input = Files.newInputStream(file)) {
|
||||||
|
byte[] bytes = input.readNBytes(MAX_BYTES + 1);
|
||||||
|
if (bytes.length > MAX_BYTES) throw new IOException("Structure census exceeds its size limit");
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private byte[] encode(Snapshot snapshot) throws IOException {
|
||||||
|
var root = new JsonObject(); root.addProperty("schema", 1); root.addProperty("world_seed", worldSeed);
|
||||||
|
var chunks = new JsonArray();
|
||||||
|
snapshot.chunks().forEach((key, flags) -> {
|
||||||
|
var entry = new JsonObject(); entry.addProperty("dimension", key.dimension());
|
||||||
|
entry.addProperty("x", key.x()); entry.addProperty("z", key.z()); entry.addProperty("flags", flags); chunks.add(entry);
|
||||||
|
});
|
||||||
|
root.add("chunks", chunks);
|
||||||
|
var sites = new JsonArray();
|
||||||
|
for (var site : snapshot.sites().values()) {
|
||||||
|
var key = site.key(); var entry = new JsonObject(); entry.addProperty("dimension", key.dimension());
|
||||||
|
entry.addProperty("id", key.id()); entry.addProperty("feature", key.feature());
|
||||||
|
entry.addProperty("x", key.x()); entry.addProperty("y", key.y()); entry.addProperty("z", key.z());
|
||||||
|
var pieces = new JsonArray();
|
||||||
|
for (var box : site.pieces()) {
|
||||||
|
var coordinates = new JsonArray();
|
||||||
|
for (int value : new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()}) coordinates.add(value);
|
||||||
|
pieces.add(coordinates);
|
||||||
|
}
|
||||||
|
entry.add("pieces", pieces); sites.add(entry);
|
||||||
|
}
|
||||||
|
root.add("sites", sites);
|
||||||
|
byte[] bytes = (JSON.toJson(root) + "\n").getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (bytes.length > MAX_BYTES) throw new IOException("Structure census exceeds its size limit");
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
private Snapshot decode(byte[] bytes) throws IOException {
|
||||||
|
String text = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString();
|
||||||
|
try (var reader = new JsonReader(new StringReader(text))) {
|
||||||
|
reader.setStrictness(Strictness.STRICT);
|
||||||
|
var root = object(read(reader, 0), Set.of("schema", "world_seed", "chunks", "sites"));
|
||||||
|
if (reader.peek() != JsonToken.END_DOCUMENT || integer(root.get("schema")) != 1 || number(root.get("world_seed")).longValueExact() != worldSeed)
|
||||||
|
throw new IOException("Structure census schema or world identity differs");
|
||||||
|
Map<ChunkKey, Integer> chunks = new HashMap<>();
|
||||||
|
for (var value : array(root.get("chunks"), StructureCensusMemory.MAX_CHUNKS)) {
|
||||||
|
var entry = object(value, Set.of("dimension", "x", "z", "flags"));
|
||||||
|
var key = new ChunkKey(string(entry.get("dimension")), integer(entry.get("x")), integer(entry.get("z")));
|
||||||
|
if (chunks.put(key, integer(entry.get("flags"))) != null) throw new IOException("Duplicate structure census chunk");
|
||||||
|
}
|
||||||
|
Map<SiteKey, Site> sites = new HashMap<>();
|
||||||
|
for (var value : array(root.get("sites"), StructureCensusMemory.MAX_SITES)) {
|
||||||
|
var entry = object(value, Set.of("dimension", "id", "feature", "x", "y", "z", "pieces"));
|
||||||
|
var key = new SiteKey(string(entry.get("dimension")), string(entry.get("id")), integer(entry.get("x")),
|
||||||
|
integer(entry.get("y")), integer(entry.get("z")), bool(entry.get("feature")));
|
||||||
|
var pieces = new ArrayList<Bounds>();
|
||||||
|
for (var coordinates : array(entry.get("pieces"), 4096)) {
|
||||||
|
var values = array(coordinates, 6); if (values.size() != 6) throw new IOException("Invalid structure bounds");
|
||||||
|
pieces.add(new Bounds(integer(values.get(0)), integer(values.get(1)), integer(values.get(2)),
|
||||||
|
integer(values.get(3)), integer(values.get(4)), integer(values.get(5))));
|
||||||
|
}
|
||||||
|
var site = new Site(key, pieces);
|
||||||
|
if (sites.put(key, site) != null) throw new IOException("Duplicate structure census site");
|
||||||
|
}
|
||||||
|
return new Snapshot(chunks, sites);
|
||||||
|
} catch (RuntimeException error) { throw io("Invalid structure census JSON", error); }
|
||||||
|
}
|
||||||
|
private static JsonElement read(JsonReader reader, int depth) throws IOException {
|
||||||
|
if (depth > 5) throw new IOException("Structure census nesting exceeds its schema");
|
||||||
|
return switch (reader.peek()) {
|
||||||
|
case BEGIN_OBJECT -> {
|
||||||
|
reader.beginObject(); var object = new JsonObject();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
String key = reader.nextName();
|
||||||
|
if (object.has(key) || object.size() >= 12) throw new IOException("Duplicate or excessive structure census fields");
|
||||||
|
object.add(key, read(reader, depth + 1));
|
||||||
|
}
|
||||||
|
reader.endObject(); yield object;
|
||||||
|
}
|
||||||
|
case BEGIN_ARRAY -> {
|
||||||
|
reader.beginArray(); var array = new JsonArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
if (array.size() >= StructureCensusMemory.MAX_CHUNKS) throw new IOException("Too many census entries");
|
||||||
|
array.add(read(reader, depth + 1));
|
||||||
|
}
|
||||||
|
reader.endArray(); yield array;
|
||||||
|
}
|
||||||
|
case STRING -> new JsonPrimitive(reader.nextString());
|
||||||
|
case NUMBER -> new JsonPrimitive(new BigDecimal(reader.nextString()));
|
||||||
|
case BOOLEAN -> new JsonPrimitive(reader.nextBoolean());
|
||||||
|
default -> throw new IOException("Unexpected structure census token");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static JsonObject object(JsonElement value, Set<String> fields) throws IOException {
|
||||||
|
if (!value.isJsonObject() || !value.getAsJsonObject().keySet().equals(fields)) throw new IOException("Missing or unknown census fields");
|
||||||
|
return value.getAsJsonObject();
|
||||||
|
}
|
||||||
|
private static JsonArray array(JsonElement value, int limit) throws IOException {
|
||||||
|
if (!value.isJsonArray() || value.getAsJsonArray().size() > limit) throw new IOException("Invalid census array");
|
||||||
|
return value.getAsJsonArray();
|
||||||
|
}
|
||||||
|
private static String string(JsonElement value) throws IOException {
|
||||||
|
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw new IOException("Expected census string");
|
||||||
|
return value.getAsString();
|
||||||
|
}
|
||||||
|
private static BigDecimal number(JsonElement value) throws IOException {
|
||||||
|
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) throw new IOException("Expected census number");
|
||||||
|
return value.getAsBigDecimal();
|
||||||
|
}
|
||||||
|
private static int integer(JsonElement value) throws IOException { return number(value).intValueExact(); }
|
||||||
|
private static boolean bool(JsonElement value) throws IOException {
|
||||||
|
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) throw new IOException("Expected census boolean");
|
||||||
|
return value.getAsBoolean();
|
||||||
|
}
|
||||||
|
private static IOException io(String message, Exception error) { return error instanceof IOException value ? value : new IOException(message, error); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
|
|
||||||
|
/** New native23 igloos are projected and certified once, before starts/references are published. */
|
||||||
|
@Mixin(IglooPieces.IglooPiece.class)
|
||||||
|
public abstract class IglooHeight23Mixin {
|
||||||
|
@ModifyArg(method = "postProcess", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;offset(III)Lnet/minecraft/core/BlockPos;"), index = 1)
|
||||||
|
private int sanctuary$alreadyProjected23(int dy) { return ExpansionStructures23.isPlacing() ? 0 : dy; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(OceanMonumentPieces.MonumentBuilding.class)
|
||||||
|
public interface MonumentChildren23Accessor {
|
||||||
|
@Accessor("childPieces") List<StructurePiece> sanctuary$children();
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import fr.koka.sanctuary.expansion.NativeStructureMetadata23;
|
||||||
|
import fr.koka.sanctuary.expansion.NativeStructurePlacement23;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
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 net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Unique;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
@Mixin(StructureStart.class)
|
||||||
|
public abstract class NativeStructureStart23Mixin implements NativeStructureMetadata23 {
|
||||||
|
@Unique private Data sanctuary$native23;
|
||||||
|
@Override public Data sanctuary$native23() { return sanctuary$native23; }
|
||||||
|
@Override public void sanctuary$native23(Data data) { sanctuary$native23 = data; }
|
||||||
|
|
||||||
|
@WrapMethod(method = "createTag")
|
||||||
|
private CompoundTag sanctuary$save23(StructurePieceSerializationContext context, ChunkPos pos, Operation<CompoundTag> original) {
|
||||||
|
CompoundTag tag = original.call(context, pos);
|
||||||
|
if (sanctuary$native23 != null && !tag.getStringOr("id", "INVALID").equals("INVALID")) {
|
||||||
|
CompoundTag metadata = new CompoundTag();
|
||||||
|
metadata.putInt("generation", 23);
|
||||||
|
metadata.putLong("island_seed", sanctuary$native23.islandSeed());
|
||||||
|
metadata.putInt("sea_level", sanctuary$native23.seaLevel());
|
||||||
|
tag.put("sanctuary_native", metadata);
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapMethod(method = "loadStaticStart")
|
||||||
|
private static StructureStart sanctuary$load23(StructurePieceSerializationContext context, CompoundTag tag, long seed,
|
||||||
|
Operation<StructureStart> original) {
|
||||||
|
StructureStart start = original.call(context, tag, seed);
|
||||||
|
Data metadata = sanctuary$read(tag);
|
||||||
|
if (start != null && start.isValid() && metadata != null) ((NativeStructureMetadata23) (Object) start).sanctuary$native23(metadata);
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapOperation(method = "loadStaticStart", at = @At(value = "INVOKE", target =
|
||||||
|
"Lnet/minecraft/world/level/levelgen/structure/structures/OceanMonumentStructure;regeneratePiecesAfterLoad(Lnet/minecraft/world/level/ChunkPos;JLnet/minecraft/world/level/levelgen/structure/pieces/PiecesContainer;)Lnet/minecraft/world/level/levelgen/structure/pieces/PiecesContainer;"))
|
||||||
|
private static PiecesContainer sanctuary$restoreMonument23(ChunkPos pos, long seed, PiecesContainer saved,
|
||||||
|
Operation<PiecesContainer> original, @Local(argsOnly = true) CompoundTag tag) {
|
||||||
|
Data metadata = sanctuary$read(tag);
|
||||||
|
if (metadata == null || saved.isEmpty()) return original.call(pos, seed, saved);
|
||||||
|
PiecesContainer restored = original.call(pos, metadata.islandSeed(), saved);
|
||||||
|
if (!restored.isEmpty()) {
|
||||||
|
int dy = saved.pieces().getFirst().getBoundingBox().minY() - restored.pieces().getFirst().getBoundingBox().minY();
|
||||||
|
for (var piece : restored.pieces()) NativeStructurePlacement23.move(piece, dy);
|
||||||
|
}
|
||||||
|
return restored;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrapMethod(method = "placeInChunk")
|
||||||
|
private void sanctuary$localWater23(WorldGenLevel level, StructureManager structures, ChunkGenerator generator,
|
||||||
|
RandomSource random, BoundingBox bounds, ChunkPos pos, Operation<Void> original) {
|
||||||
|
if (sanctuary$native23 == null) { original.call(level, structures, generator, random, bounds, pos); return; }
|
||||||
|
Integer previous = NativeStructurePlacement23.enter(sanctuary$native23.seaLevel());
|
||||||
|
try { original.call(level, structures, generator, random, bounds, pos); }
|
||||||
|
finally { NativeStructurePlacement23.restore(previous); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Unique private static Data sanctuary$read(CompoundTag tag) {
|
||||||
|
CompoundTag metadata = tag.getCompoundOrEmpty("sanctuary_native");
|
||||||
|
return metadata.getIntOr("generation", 0) == 23
|
||||||
|
? new Data(metadata.getLongOr("island_seed", 0L), metadata.getIntOr("sea_level", -64)) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.expansion.NativeStructurePlacement23;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
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(WorldGenRegion.class)
|
||||||
|
public abstract class NativeStructureWater23Mixin {
|
||||||
|
@Inject(method = "getSeaLevel", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void sanctuary$localWater23(CallbackInfoReturnable<Integer> cir) {
|
||||||
|
Integer seaLevel = NativeStructurePlacement23.seaLevel();
|
||||||
|
if (seaLevel == null) seaLevel = fr.koka.sanctuary.expansion.ExpansionOcean23.decorationSeaLevel();
|
||||||
|
if (seaLevel != null) cir.setReturnValue(seaLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.RuinedPortalPiece;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(RuinedPortalPiece.class)
|
||||||
|
public interface RuinedPortal23Accessor {
|
||||||
|
@Accessor("verticalPlacement") RuinedPortalPiece.VerticalPlacement sanctuary$verticalPlacement();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(ScatteredFeaturePiece.class)
|
||||||
|
public interface ScatteredHeight23Accessor {
|
||||||
|
@Accessor("heightPosition") void sanctuary$heightPosition(int y);
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.level.GenerationChunkHolder;
|
||||||
|
import net.minecraft.util.StaticCache2D;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatusTasks;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStep;
|
||||||
|
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||||
|
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(ChunkStatusTasks.class)
|
||||||
|
public abstract class StructureCensusFeaturesMixin {
|
||||||
|
@Inject(method = "generateFeatures", at = @At("RETURN"))
|
||||||
|
private static void sanctuary$recordSuccessfulFeatures(WorldGenContext context, ChunkStep step,
|
||||||
|
StaticCache2D<GenerationChunkHolder> cache, ChunkAccess chunk, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> cir) {
|
||||||
|
if (!SharedConstants.DEBUG_DISABLE_FEATURES) cir.getReturnValue().thenAccept(completed -> StructureCensusService.observeFeatures(context.level(), completed));
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||||
|
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(MonsterRoomFeature.class)
|
||||||
|
public abstract class StructureCensusMonsterRoomMixin {
|
||||||
|
@Inject(method = "place", at = @At("RETURN"))
|
||||||
|
private void sanctuary$recordSuccessfulRoom(WorldGenLevel level, ChunkGenerator generator, RandomSource random,
|
||||||
|
BlockPos origin, CallbackInfoReturnable<Boolean> cir) {
|
||||||
|
if (Boolean.TRUE.equals(cir.getReturnValue())) StructureCensusService.observeMonsterRoom(level, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(ServerLevel.class)
|
||||||
|
public abstract class StructureCensusStartsMixin {
|
||||||
|
@Inject(method = "onStructureStartsAvailable", at = @At("RETURN"))
|
||||||
|
private void sanctuary$recordNativeStarts(ChunkAccess chunk, CallbackInfo ci) {
|
||||||
|
StructureCensusService.observeStarts((ServerLevel) (Object) this, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -27,7 +27,8 @@ public abstract class UnifiedDecorationProtectionMixin {
|
|||||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing()
|
||||||
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
|
&& !fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.isPlacing()
|
||||||
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing() && generator.protectsCityDecoration(pos))
|
&& !fr.koka.sanctuary.worldgen.transit.TransitStructures21.isPlacing()
|
||||||
|
&& !fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing() && generator.protectsCityDecoration(pos))
|
||||||
callback.setReturnValue(false);
|
callback.setReturnValue(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +39,23 @@ public abstract class UnifiedDecorationProtectionMixin {
|
|||||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
|
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
|
||||||
// Vanilla rejects writes outside its region before reading the destination chunk.
|
// Vanilla rejects writes outside its region before reading the destination chunk.
|
||||||
if (!region.isWithinWriteZone(pos)) return;
|
if (!region.isWithinWriteZone(pos)) return;
|
||||||
|
if (generator.rootGeneration() == 23) {
|
||||||
|
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
|
||||||
|
var island = generator.islandAt(pos.getX(), pos.getZ());
|
||||||
|
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());
|
||||||
|
if (fr.koka.sanctuary.expansion.ExpansionOcean23.retains(ocean, pos.getY())
|
||||||
|
&& (!state.isSolid() || !state.getFluidState().isEmpty())) {
|
||||||
|
callback.setReturnValue(false); return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
|
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
|
||||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||||
// native pieces may replace water there; published generations retain the old rule.
|
// 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)
|
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
|
||||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||||
&& generator.protectsCityDecoration(pos)) return;
|
&& generator.protectsCityDecoration(pos)) return;
|
||||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
|||||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 22)));
|
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 22)));
|
||||||
|
|
||||||
|
public static final MapCodec<SanctuaryBiomeSource> SECRETS23_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||||
|
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||||
|
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 23)));
|
||||||
|
|
||||||
private final List<Holder<Biome>> allBiomes;
|
private final List<Holder<Biome>> allBiomes;
|
||||||
private final PopulationIslandBiomeSource population;
|
private final PopulationIslandBiomeSource population;
|
||||||
private final boolean cities;
|
private final boolean cities;
|
||||||
@@ -73,6 +77,9 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
|||||||
this.generation = generation;
|
this.generation = generation;
|
||||||
this.cities = ExpansionIsland.spatialGeneration(generation);
|
this.cities = ExpansionIsland.spatialGeneration(generation);
|
||||||
allBiomes = List.copyOf(biomes);
|
allBiomes = List.copyOf(biomes);
|
||||||
|
if (generation == 23) 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,
|
cityBiome = cities ? required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
|
||||||
fr.koka.sanctuary.SanctuaryMod.id("lost_city"))) : null;
|
fr.koka.sanctuary.SanctuaryMod.id("lost_city"))) : null;
|
||||||
population = new PopulationIslandBiomeSource(
|
population = new PopulationIslandBiomeSource(
|
||||||
@@ -91,7 +98,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
|||||||
cityPlans = java.util.Map.copyOf(plans);
|
cityPlans = java.util.Map.copyOf(plans);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return 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 == 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) {
|
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||||
BiomeResolver woodlands = population.createResolver(sampler);
|
BiomeResolver woodlands = population.createResolver(sampler);
|
||||||
|
|||||||
+80
-40
@@ -69,6 +69,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 22, experimental)));
|
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 22, experimental)));
|
||||||
|
|
||||||
|
public static final MapCodec<SanctuaryChunkGenerator> SECRETS23_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||||
|
SanctuaryBiomeSource.SECRETS23_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, 23, experimental)));
|
||||||
|
|
||||||
|
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
|
||||||
|
private boolean mineralPrepared23;
|
||||||
|
public fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23() { return mineralSanctuary23; }
|
||||||
private final boolean experimentalStructures;
|
private final boolean experimentalStructures;
|
||||||
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
||||||
private boolean ruinsPrepared;
|
private boolean ruinsPrepared;
|
||||||
@@ -121,12 +130,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
}
|
}
|
||||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||||
super(source, initialDiameter, true);
|
super(source, initialDiameter, true);
|
||||||
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && experimental;
|
this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && experimental;
|
||||||
this.generation = generation;
|
this.generation = generation;
|
||||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||||
}
|
}
|
||||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return 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 == 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) {
|
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||||
}
|
}
|
||||||
@@ -137,10 +146,11 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||||
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
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) {
|
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
return (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
return generation == 23 && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|| (generation == 21 || generation == 22) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|| generation == 22 && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||||
|
|| (generation == 22 || generation == 23) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
}
|
}
|
||||||
public IslandCapacity capacity() {
|
public IslandCapacity capacity() {
|
||||||
return switch (initialDiameter()) {
|
return switch (initialDiameter()) {
|
||||||
@@ -175,7 +185,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
// Publish one root context before any generation workers use it.
|
// Publish one root context before any generation workers use it.
|
||||||
context(islands.getFirst());
|
context(islands.getFirst());
|
||||||
prepareCities(islands);
|
prepareCities(islands);
|
||||||
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||||
|
prepareMineral23(islands);
|
||||||
prepareRuins(islands);
|
prepareRuins(islands);
|
||||||
prepareHeritage(islands);
|
prepareHeritage(islands);
|
||||||
prepareTransit(islands);
|
prepareTransit(islands);
|
||||||
@@ -183,7 +194,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||||
// Complete and publish city geometry before workers can observe the new owner list.
|
// Complete and publish city geometry before workers can observe the new owner list.
|
||||||
prepareCities(islands);
|
prepareCities(islands);
|
||||||
if (generation == 22) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||||
|
prepareMineral23(islands);
|
||||||
prepareRuins(islands);
|
prepareRuins(islands);
|
||||||
prepareHeritage(islands);
|
prepareHeritage(islands);
|
||||||
prepareTransit(islands);
|
prepareTransit(islands);
|
||||||
@@ -197,7 +209,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
source().setCityPlans(java.util.Map.of());
|
source().setCityPlans(java.util.Map.of());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && !cityStructuresEnabled) {
|
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && !cityStructuresEnabled) {
|
||||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||||
activeCityPlans = java.util.Map.of();
|
activeCityPlans = java.util.Map.of();
|
||||||
source().setCityPlans(java.util.Map.of());
|
source().setCityPlans(java.util.Map.of());
|
||||||
@@ -213,7 +225,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
}
|
}
|
||||||
cityProtection15 = protection.build();
|
cityProtection15 = protection.build();
|
||||||
}
|
}
|
||||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) {
|
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) {
|
||||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||||
for (var island : islands) {
|
for (var island : islands) {
|
||||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||||
@@ -228,29 +240,33 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
source().setCityPlans(published);
|
source().setCityPlans(published);
|
||||||
}
|
}
|
||||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||||
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
var terrain = context(root);
|
var terrain = context(root);
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
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()));
|
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
var plan = generation == 23
|
||||||
|
? 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;
|
ruinsPlan17 = plan;
|
||||||
ruinsPrepared = true;
|
ruinsPrepared = true;
|
||||||
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
|
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
|
||||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||||
}
|
}
|
||||||
private void prepareHeritage(List<ExpansionIsland> islands) {
|
private void prepareHeritage(List<ExpansionIsland> islands) {
|
||||||
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
var terrain = context(root);
|
var terrain = context(root);
|
||||||
if (generation == 21 || generation == 22) {
|
if (generation == 21 || generation == 22 || generation == 23) {
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.heritage.HeritageTerrain21(root, terrain.generator(), terrain.random(),
|
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())
|
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||||
var plan = generation == 22
|
var plan = generation == 23
|
||||||
|
? 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)
|
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||||
: fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
: fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||||
heritagePlan19 = plan;
|
heritagePlan19 = plan;
|
||||||
@@ -273,18 +289,18 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||||
}
|
}
|
||||||
private void prepareTransit(List<ExpansionIsland> islands) {
|
private void prepareTransit(List<ExpansionIsland> islands) {
|
||||||
if ((generation != 21 && generation != 22) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
|
if ((generation != 21 && generation != 22 && generation != 23) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
var terrain = context(root);
|
var terrain = context(root);
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
var sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
|
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
transitPlan21 = generation == 22
|
transitPlan21 = generation >= 22
|
||||||
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
? 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);
|
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||||
int directColumns = 0;
|
int directColumns = 0;
|
||||||
if (generation == 22 && transitPlan21.isEmpty()) {
|
if ((generation == 22 || generation == 23) && transitPlan21.isEmpty()) {
|
||||||
directColumns = sampled.columnsUsed();
|
directColumns = sampled.columnsUsed();
|
||||||
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
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()));
|
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
@@ -295,11 +311,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
|
transitPlan21.modules().size(), transitPlan21.samples(), directColumns + sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000, transitPlan21.diagnostic());
|
||||||
}
|
}
|
||||||
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
|
private boolean protectsCore22(net.minecraft.core.BlockPos pos) {
|
||||||
return generation == 22 && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
return (generation == 22 || generation == 23) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ()));
|
|| undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|
|| generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||||
}
|
}
|
||||||
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
|
private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
|
||||||
if (generation != 22 || !cityStructuresEnabled || undergroundHallPrepared22) return;
|
if ((generation != 22 && generation != 23) || !cityStructuresEnabled || undergroundHallPrepared22) return;
|
||||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||||
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||||
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
|
.anyMatch(m -> m.kind() == fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL)) {
|
||||||
@@ -309,20 +326,40 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
|
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
|
||||||
pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) || protectsCore22(pos));
|
pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) || protectsCore22(pos));
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
undergroundHall22 = fr.koka.sanctuary.worldgen.city.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
|
var hallTerrain = new fr.koka.sanctuary.worldgen.city.LostCityUnderground22.Terrain() {
|
||||||
new fr.koka.sanctuary.worldgen.city.LostCityUnderground22.Terrain() {
|
|
||||||
@Override public int surfaceY(int x, int z) { var ground = sampled.ground(x, z); return ground.dry() ? ground.top() : -1; }
|
@Override public int surfaceY(int x, int z) { var ground = sampled.ground(x, z); return ground.dry() ? ground.top() : -1; }
|
||||||
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||||
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||||
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
|
@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); }
|
@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
|
||||||
|
? 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();
|
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||||
protection.add(fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY, undergroundHall22);
|
protection.add(fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY, undergroundHall22);
|
||||||
undergroundHallProtection22 = protection.build(); undergroundHallPrepared22 = true;
|
undergroundHallProtection22 = protection.build(); undergroundHallPrepared22 = true;
|
||||||
SanctuaryMod.LOGGER.info("Independent hall22 seed {}: {} networks, {} probes, {} columns in {} ms", root.seed(),
|
SanctuaryMod.LOGGER.info("Independent hall{} seed {}: {} networks, {} probes, {} columns in {} ms", generation, root.seed(),
|
||||||
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
undergroundHall22.networks().size(), undergroundHall22.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||||
}
|
}
|
||||||
|
private void prepareMineral23(List<ExpansionIsland> islands) {
|
||||||
|
if (generation != 23 || !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,
|
||||||
|
pos -> protectsDecoration(pos) || protectsCore22(pos));
|
||||||
|
long started = System.nanoTime();
|
||||||
|
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
|
||||||
|
new fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.Terrain() {
|
||||||
|
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||||
|
@Override public int surfaceY(int x, int z) { return sampled.ground(x, z).top(); }
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); }
|
||||||
|
});
|
||||||
|
mineralPrepared23 = true;
|
||||||
|
SanctuaryMod.LOGGER.info("Mineral sanctuary23 seed {}: accepted={}, columns={}, {} ms; {}", root.seed(),
|
||||||
|
!mineralSanctuary23.isEmpty(), sampled.columnsUsed(), (System.nanoTime() - started) / 1_000_000, mineralSanctuary23.diagnostic());
|
||||||
|
}
|
||||||
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
||||||
// State and receipt do not change the terrain geometry used by a cached plan.
|
// State and receipt do not change the terrain geometry used by a cached plan.
|
||||||
var ready = island.status().equals("ready") ? island : island.ready();
|
var ready = island.status().equals("ready") ? island : island.ready();
|
||||||
@@ -344,15 +381,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public fr.koka.sanctuary.worldgen.city.LostCityPlan lostCityPlan(ExpansionIsland island) {
|
public fr.koka.sanctuary.worldgen.city.LostCityPlan lostCityPlan(ExpansionIsland island) {
|
||||||
|
if (generation == 23 && island.origin()) return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||||
if (!ExpansionIsland.spatialGeneration(generation) || island.generation() != generation || (!island.origin() && !island.climate().equals("lost_city")))
|
if (!ExpansionIsland.spatialGeneration(generation) || island.generation() != generation || (!island.origin() && !island.climate().equals("lost_city")))
|
||||||
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
var terrain = context(island);
|
var terrain = context(island);
|
||||||
if (generation == 21 || generation == 22) {
|
if (generation == 21 || generation == 22 || generation == 23) {
|
||||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
|
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
|
||||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||||
var placement = generation == 22
|
var placement = generation >= 22
|
||||||
? fr.koka.sanctuary.worldgen.city.LostCityPlanner22.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled)
|
? fr.koka.sanctuary.worldgen.city.LostCityPlanner22.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled)
|
||||||
: fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled);
|
: fr.koka.sanctuary.worldgen.city.LostCityPlanner21.plan(island.seed(), island.centerX(), island.centerZ(), island.diameter(), island.origin(), sampled);
|
||||||
var surface = placement.surface();
|
var surface = placement.surface();
|
||||||
@@ -454,11 +492,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
if (generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| (generation == 21 || generation == 22) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|| generation == 22 && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||||
|
|| (generation == 22 || generation == 23) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||||
if (generation != 14) return false;
|
if (generation != 14) return false;
|
||||||
var island = islandAt(pos.getX(), pos.getZ());
|
var island = islandAt(pos.getX(), pos.getZ());
|
||||||
@@ -480,20 +519,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||||
}
|
}
|
||||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
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) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
}
|
}
|
||||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) 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) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||||
}
|
}
|
||||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return List.of();
|
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return List.of();
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||||
}
|
}
|
||||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22) return null;
|
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return null;
|
||||||
lostCityPlan(island);
|
lostCityPlan(island);
|
||||||
return cityReservations16.get(cityIdentity(island));
|
return cityReservations16.get(cityIdentity(island));
|
||||||
}
|
}
|
||||||
@@ -503,15 +542,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(
|
@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,
|
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
|
||||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
|
if (generation == 23) 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))
|
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);
|
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))
|
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);
|
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && 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) && 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);
|
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
if ((generation == 19 || generation == 20 || generation == 21 || generation == 22) && 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) && 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);
|
return fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
if ((generation == 21 || generation == 22) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY))
|
if ((generation == 21 || generation == 22 || generation == 23) && 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 fr.koka.sanctuary.worldgen.transit.TransitStructures21.findNearest(level, structures, origin, radius, skipReferenced);
|
||||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||||
}
|
}
|
||||||
@@ -540,7 +580,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
|||||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22) && cityStructuresEnabled) {
|
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && cityStructuresEnabled) {
|
||||||
var reserved = cityReservations16.get(cityIdentity(island));
|
var reserved = cityReservations16.get(cityIdentity(island));
|
||||||
if (reserved != null) {
|
if (reserved != null) {
|
||||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ public final class LostCityStructure extends Structure {
|
|||||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||||
}
|
}
|
||||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
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.cityReservation16(island) != null
|
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) && generator.cityReservation16(island) != null
|
||||||
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
||||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||||
}
|
}
|
||||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22)) 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)) for (var support : generator.citySupports16(island)) {
|
||||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||||
}
|
}
|
||||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? 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) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? 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) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
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)
|
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
.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)) 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)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
network.modules().stream().filter(m -> m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.city;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** An independent public undercroft. No surface district, fake parcel or city reservation is needed.
|
||||||
|
* Only complete hall/access pairs are published; optional utility wings cannot invalidate them. */
|
||||||
|
public final class LostCityUnderground23 {
|
||||||
|
public static final int MAX_COLUMNS = 6_000, MAX_SAMPLES = LostCityUnderground16.MAX_SAMPLES;
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private LostCityUnderground23() {}
|
||||||
|
|
||||||
|
|
||||||
|
public static Plan plan(long seed, int cx, int cz, int diameter, LostCityUnderground22.Terrain terrain) {
|
||||||
|
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported undercroft island size");
|
||||||
|
var probe = new Probe(terrain, cx, cz, diameter);
|
||||||
|
List<Module> accepted = null;
|
||||||
|
long acceptedSeed = seed;
|
||||||
|
try {
|
||||||
|
search: for (int attempt = 0; attempt < 40; attempt++) {
|
||||||
|
double angle = (mix(seed ^ 0x22A11L) >>> 11) * 0x1.0p-53 * Math.PI * 2 + attempt * 2.399963229728653;
|
||||||
|
double radius = diameter * (attempt < 12 ? .15 : attempt < 26 ? .23 : .31);
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
if (Math.hypot(x - cx, z - cz) < 56) continue;
|
||||||
|
int top = accessHeight(x, z, probe);
|
||||||
|
if (top < 0) continue;
|
||||||
|
for (int drop : new int[]{22, 32, 46, 62, 86, 110}) {
|
||||||
|
int floor = top - drop;
|
||||||
|
if (floor < 24) continue;
|
||||||
|
long networkSeed = mix(seed ^ ((long) x << 32) ^ z ^ drop);
|
||||||
|
for (int turn = 0; turn < 4; turn++) {
|
||||||
|
int direction = (int) Math.floorMod(networkSeed + turn, 4);
|
||||||
|
var access = new Module(Kind.ACCESS, x - 3, floor - 1, z - 3, 7, drop + 4, 7,
|
||||||
|
1 << direction, networkSeed, List.of());
|
||||||
|
var hall = attached(access, Kind.EXPANSION_HALL, direction, mix(networkSeed + 1));
|
||||||
|
// Cheap hall witnesses come before repeatedly scanning a tall access shaft.
|
||||||
|
if (!coarse(hall, probe)) continue;
|
||||||
|
var modules = new ArrayList<Module>();
|
||||||
|
access = certify(access, probe);
|
||||||
|
if (access == null) continue;
|
||||||
|
modules.add(access);
|
||||||
|
if (!attach(modules, 0, hall, direction, probe)) continue;
|
||||||
|
accepted = modules; acceptedSeed = networkSeed;
|
||||||
|
break search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { probe.reject("required-search-budget"); }
|
||||||
|
if (accepted != null) {
|
||||||
|
// Utility galleries remain connected to the hall; swimming pools are not part of the23 selection.
|
||||||
|
int forward = Integer.numberOfTrailingZeros(accepted.getFirst().openings());
|
||||||
|
try {
|
||||||
|
for (int side : new int[]{(forward + 1) & 3, (forward + 3) & 3})
|
||||||
|
if (attach(accepted, 1, attached(accepted.get(1), Kind.SEWER, side, mix(acceptedSeed + 2)), side, probe)) break;
|
||||||
|
} catch (BudgetExceeded exhausted) { probe.reject("optional-wing-budget"); }
|
||||||
|
}
|
||||||
|
var networks = accepted == null ? List.<Network>of() : List.of(new Network(
|
||||||
|
accepted.getFirst().centerX() >> 4, accepted.getFirst().centerZ() >> 4, acceptedSeed, accepted));
|
||||||
|
System.getLogger(LostCityUnderground23.class.getName()).log(System.Logger.Level.INFO,
|
||||||
|
"Independent undercroft23 seed=" + seed + " networks=" + networks.size() + " columns=" + probe.columns.size()
|
||||||
|
+ " samples=" + probe.samples + " kinds=" + (accepted == null ? List.of() : accepted.stream().map(Module::kind).toList())
|
||||||
|
+ " admission=" + probe.rejections);
|
||||||
|
return new Plan(networks, probe.samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The whole visible seven-square hatch deck stays within two blocks of existing dry ground. */
|
||||||
|
private static int accessHeight(int cx, int cz, Probe p) {
|
||||||
|
int low = 384, high = -1;
|
||||||
|
for (int z = cz - 3; z <= cz + 3; z++) for (int x = cx - 3; x <= cx + 3; x++) {
|
||||||
|
int top = p.surface(x, z);
|
||||||
|
if (top < 64 || top > 365 || !p.solid(x, top, z) || p.protectedAt(x, top, z)) return p.rejectHeight("dry-access");
|
||||||
|
low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
if (high - low > 2) return p.rejectHeight("access-relief");
|
||||||
|
}
|
||||||
|
return high;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Module attached(Module parent, Kind kind, int direction, long seed) {
|
||||||
|
int width = kind == Kind.POOLROOM ? 57 : kind == Kind.EXPANSION_HALL ? 25 : 9;
|
||||||
|
int depth = kind == Kind.POOLROOM ? 31 : width;
|
||||||
|
int height = kind == Kind.POOLROOM ? 13 : kind == Kind.EXPANSION_HALL ? 10 : 7;
|
||||||
|
int offset = (DX[direction] == 0 ? parent.depth() / 2 + depth / 2 : parent.width() / 2 + width / 2) + 8;
|
||||||
|
int x = parent.centerX() + DX[direction] * offset, z = parent.centerZ() + DZ[direction] * offset;
|
||||||
|
return new Module(kind, x - width / 2, parent.floorY() - (kind == Kind.POOLROOM ? 5 : 1),
|
||||||
|
z - depth / 2, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
|
||||||
|
}
|
||||||
|
private static boolean attach(List<Module> modules, int parentIndex, Module room, int direction, Probe p) {
|
||||||
|
var parent = modules.get(parentIndex);
|
||||||
|
int x = DX[direction] == 0 ? parent.centerX() - 2 : direction == 1 ? parent.maxX() + 1 : room.maxX() + 1;
|
||||||
|
int z = DZ[direction] == 0 ? parent.centerZ() - 2 : direction == 2 ? parent.maxZ() + 1 : room.maxZ() + 1;
|
||||||
|
var passage = new Module(Kind.PASSAGE, x, parent.floorY() - 1, z, DX[direction] == 0 ? 5 : 7, 6,
|
||||||
|
DZ[direction] == 0 ? 5 : 7, (1 << direction) | (1 << ((direction + 2) & 3)), mix(room.seed()), List.of());
|
||||||
|
for (var old : modules) if (overlaps(room, old) || overlaps(passage, old)) { p.reject("wing-collision"); return false; }
|
||||||
|
if (!coarse(room, p) || !coarse(passage, p)) return false;
|
||||||
|
room = certify(room, p); if (room == null) return false;
|
||||||
|
passage = certify(passage, p); if (passage == null) return false;
|
||||||
|
if (LostCityUnderground16.supportConflict(room, passage)) return false;
|
||||||
|
for (var old : modules) if (LostCityUnderground16.supportConflict(room, old)
|
||||||
|
|| LostCityUnderground16.supportConflict(passage, old)) return false;
|
||||||
|
modules.set(parentIndex, new Module(parent.kind(), parent.minX(), parent.minY(), parent.minZ(), parent.width(),
|
||||||
|
parent.height(), parent.depth(), parent.openings() | 1 << direction, parent.seed(), parent.supports()));
|
||||||
|
modules.add(room); modules.add(passage);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static boolean overlaps(Module a, Module b) {
|
||||||
|
return a.minX() <= b.maxX() && a.maxX() >= b.minX() && a.minZ() <= b.maxZ() && a.maxZ() >= b.minZ()
|
||||||
|
&& a.boundsMinY() <= b.boundsMaxY() && a.boundsMaxY() >= b.boundsMinY();
|
||||||
|
}
|
||||||
|
private static boolean coarse(Module m, Probe p) {
|
||||||
|
int solid = 0;
|
||||||
|
for (int x : new int[]{m.minX(), m.centerX(), m.maxX()}) for (int z : new int[]{m.minZ(), m.centerZ(), m.maxZ()}) {
|
||||||
|
if (!p.owns(x, z)) { p.reject("outside-island"); return false; }
|
||||||
|
for (int y : new int[]{m.minY(), m.floorY() + 2, m.maxY()}) {
|
||||||
|
if (p.protectedAt(x, y, z)) { p.reject("coarse-protected"); return false; }
|
||||||
|
if (p.solid(x, y, z)) solid++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (solid < 5) { p.reject("coarse-rock"); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static Module certify(Module m, Probe p) {
|
||||||
|
var supports = new ArrayList<Support>();
|
||||||
|
int stride = m.kind() == Kind.EXPANSION_HALL || m.kind() == Kind.POOLROOM ? 8 : 3;
|
||||||
|
for (int z : lattice(m.minZ(), m.maxZ(), stride)) for (int x : lattice(m.minX(), m.maxX(), stride)) {
|
||||||
|
var support = support(m, x, z, p);
|
||||||
|
if (support == null) { p.reject("no-short-anchor"); return null; }
|
||||||
|
supports.add(support);
|
||||||
|
}
|
||||||
|
int solid = 0, count = 0;
|
||||||
|
for (int z = m.minZ(); z <= m.maxZ(); z++) for (int x = m.minX(); x <= m.maxX(); x++) {
|
||||||
|
if (!p.owns(x, z)) { p.reject("outside-island"); return null; }
|
||||||
|
for (int y = m.minY(); y <= m.maxY(); y++) {
|
||||||
|
if (p.protectedAt(x, y, z)) { p.reject("protected-volume"); return null; }
|
||||||
|
if (p.solid(x, y, z)) solid++; count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The constructed shell can cross a native cavity; it must still be embedded in real terrain.
|
||||||
|
if (solid * 100L < count * (m.kind() == Kind.ACCESS ? 50 : 20)) { p.reject("insufficient-rock"); return null; }
|
||||||
|
return new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
|
||||||
|
m.openings(), m.seed(), supports);
|
||||||
|
}
|
||||||
|
private static Support support(Module m, int x, int z, Probe p) {
|
||||||
|
// Save even direct bearings: the existing protection builder then retains their untouched rock.
|
||||||
|
for (int distance = 1; distance <= 8; distance++) {
|
||||||
|
int y = m.minY() - distance;
|
||||||
|
if (y < 2 || p.protectedAt(x, y, z)) break;
|
||||||
|
if (p.solid(x, y, z) && p.solid(x, y - 1, z) && !p.protectedAt(x, y - 1, z))
|
||||||
|
return new Support(Type.PILLAR, x, y, z, m.minY() - 1);
|
||||||
|
}
|
||||||
|
for (int distance = 1; distance <= 8; distance++) {
|
||||||
|
int y = m.maxY() + distance;
|
||||||
|
if (y > 381 || p.protectedAt(x, y, z)) break;
|
||||||
|
if (p.solid(x, y, z) && p.solid(x, y + 1, z) && !p.protectedAt(x, y + 1, z))
|
||||||
|
return new Support(Type.CHAIN, x, m.maxY() + 1, z, y);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private static List<Integer> lattice(int min, int max, int stride) {
|
||||||
|
var result = new ArrayList<Integer>(); for (int n = min; n <= max; n += stride) result.add(n);
|
||||||
|
if (result.getLast() != max) result.add(max); return result;
|
||||||
|
}
|
||||||
|
private static final class Probe {
|
||||||
|
final LostCityUnderground22.Terrain terrain; final int cx, cz; final double radius;
|
||||||
|
final Map<Long, Integer> columns = new HashMap<>(); final Map<String, Integer> rejections = new LinkedHashMap<>(); int samples;
|
||||||
|
Probe(LostCityUnderground22.Terrain terrain, int cx, int cz, int diameter) { this.terrain = terrain; this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 4; }
|
||||||
|
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded(); samples++; }
|
||||||
|
boolean owns(int x, int z) { return Math.hypot((double) x - cx, (double) z - cz) <= radius && terrain.owns(x, z); }
|
||||||
|
int surface(int x, int z) {
|
||||||
|
if (!owns(x, z)) return -1;
|
||||||
|
long key = ((long) x << 32) ^ (z & 0xffffffffL); var value = columns.get(key); if (value != null) return value;
|
||||||
|
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded();
|
||||||
|
count(); int y = terrain.surfaceY(x, z); columns.put(key, y); return y;
|
||||||
|
}
|
||||||
|
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return false; count(); return terrain.solid(x, y, z); }
|
||||||
|
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) < 0) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||||
|
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
|
||||||
|
int rejectHeight(String reason) { reject(reason); return -1; }
|
||||||
|
}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException { private static final long serialVersionUID = 1L; }
|
||||||
|
private static long mix(long x) { x = (x ^ x >>> 30) * 0xbf58476d1ce4e5b9L; x = (x ^ x >>> 27) * 0x94d049bb133111ebL; return x ^ x >>> 31; }
|
||||||
|
}
|
||||||
+2
-2
@@ -14,9 +14,9 @@ public final class LostCityUndergroundStructure22 extends Structure {
|
|||||||
|
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| generator.rootGeneration() != 22) return Optional.empty();
|
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != 22) return Optional.empty();
|
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23)) return Optional.empty();
|
||||||
var networks = generator.undergroundHall22().networks().stream()
|
var networks = generator.undergroundHall22().networks().stream()
|
||||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||||
if (networks.isEmpty()) return Optional.empty();
|
if (networks.isEmpty()) return Optional.empty();
|
||||||
|
|||||||
+4
-4
@@ -41,9 +41,9 @@ public final class LostCityUndergroundStructures22 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 22) return;
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != 22) return;
|
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23)) return;
|
||||||
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
if (sanctuary.undergroundHall22().networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
@@ -63,14 +63,14 @@ public final class LostCityUndergroundStructures22 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| generator.rootGeneration() != 22 || !level.structureManager().shouldGenerateStructures()) return null;
|
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
var candidates = new ArrayList<LostCityUnderground16.Network>();
|
var candidates = new ArrayList<LostCityUnderground16.Network>();
|
||||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||||
for (var network : generator.undergroundHall22().networks()) {
|
for (var network : generator.undergroundHall22().networks()) {
|
||||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||||
if (island == null || !island.origin() || island.generation() != 22 || !island.status().equals("ready")) continue;
|
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23) || !island.status().equals("ready")) continue;
|
||||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
|
|||||||
var level = source.getLevel();
|
var level = source.getLevel();
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) {
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) {
|
||||||
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -28,16 +28,20 @@ public final class HeritageCommands19 {
|
|||||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||||
if (generator.rootGeneration() == 21 || generator.rootGeneration() == 22) for (var module : generator.transitPlan21().modules()) {
|
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var module : generator.transitPlan21().modules()) {
|
||||||
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
|
if (module.kind() != fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS) continue;
|
||||||
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
|
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
|
||||||
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
|
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||||
}
|
}
|
||||||
if (generator.rootGeneration() == 22) for (var network : generator.undergroundHall22().networks()) {
|
if ((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) for (var network : generator.undergroundHall22().networks()) {
|
||||||
var at = fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.locator(network);
|
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);
|
source.sendSuccess(() -> Component.translatable("sanctuary.underground_hall.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||||
}
|
}
|
||||||
return plan.sites().size() + (generator.rootGeneration() == 22
|
if (generator.rootGeneration() == 23 && !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.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
|
||||||
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
|
? (int) generator.transitPlan21().modules().stream().filter(m -> m.kind() == fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind.ACCESS).count()
|
||||||
+ generator.undergroundHall22().networks().size() : 0);
|
+ generator.undergroundHall22().networks().size() : 0);
|
||||||
}))));
|
}))));
|
||||||
|
|||||||
+685
@@ -0,0 +1,685 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.heritage;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.HeritagePlanner21.Terrain;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Generation23 only: one observatory and independent abandoned rail axes. A finite catalogue is admitted before complete, repairable routes are published.
|
||||||
|
* Everything samples native columns; no chunk, player object or future continent is requested. */
|
||||||
|
public final class HeritagePlanner23 {
|
||||||
|
public static final int MAX_COLUMNS = HeritagePlan19.MAX_COLUMNS, MAX_SAMPLES = HeritagePlan19.MAX_SAMPLES;
|
||||||
|
// Prefer an existing platform: at most two blocks of cut/fill and four of total relief.
|
||||||
|
public static final int MAX_SITE_CUT = 2, MAX_SITE_FILL = 2, MAX_SITE_RELIEF = 4;
|
||||||
|
public static final int MAX_ROUTE_CUT = 1, MAX_ROUTE_FILL = 1;
|
||||||
|
private static final int SITE_COLUMNS = 11_000, SITE_SAMPLES = 540_000;
|
||||||
|
private static final int INVALID = Integer.MIN_VALUE;
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private HeritagePlanner23() {}
|
||||||
|
|
||||||
|
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||||
|
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage23 requires a starter size");
|
||||||
|
var probe = new Probe(terrain, cx, cz, diameter);
|
||||||
|
probe.columnLimit = SITE_COLUMNS; probe.sampleLimit = SITE_SAMPLES;
|
||||||
|
List<Landmark> landmarks = new ArrayList<>(List.copyOf(terrain.landmarks()).stream().limit(12).toList());
|
||||||
|
naturalRailEndpoints(seed, cx, cz, diameter, landmarks, probe);
|
||||||
|
var sites = new ArrayList<Site>(); var cells = new LinkedHashMap<Long, OwnedCell>(); var diagnostics = new EnumMap<Kind, String>(Kind.class);
|
||||||
|
for (var kind : List.of(Kind.OBSERVATORY)) {
|
||||||
|
int remaining = 0;
|
||||||
|
probe.columnLimit = SITE_COLUMNS - remaining * (SITE_COLUMNS / 14);
|
||||||
|
probe.sampleLimit = SITE_SAMPLES - remaining * (SITE_SAMPLES / 14);
|
||||||
|
SiteDraft admitted = null; int attempts = 0;
|
||||||
|
var reasons = new java.util.TreeMap<String, Integer>(); probe.reasons = reasons;
|
||||||
|
try {
|
||||||
|
for (var candidate : candidates(seed, kind, cx, cz, diameter, sites, landmarks, probe)) {
|
||||||
|
attempts++;
|
||||||
|
admitted = site(seed, kind, candidate, sites, landmarks, cells, probe);
|
||||||
|
if (admitted != null) break;
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { reasons.merge(exhausted.columns ? "column-budget" : "probe-budget", 1, Integer::sum); }
|
||||||
|
if (admitted != null) {
|
||||||
|
sites.add(admitted.site());
|
||||||
|
Request landing = new Request(admitted.site().id() + "_landing", admitted.site().id(), admitted.site().id(), RouteKind.PATH);
|
||||||
|
for (var cell : admitted.landings()) cells.put(key(cell.x(), cell.z()), new OwnedCell(landing, cell));
|
||||||
|
}
|
||||||
|
diagnostics.put(kind, (admitted == null ? "not admitted" : "admitted") + "; candidates=" + attempts + "; " + reasons);
|
||||||
|
}
|
||||||
|
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||||
|
var links = new ArrayList<Link>();
|
||||||
|
naturalRailFragments(seed, landmarks, sites, links, cells, probe);
|
||||||
|
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
|
||||||
|
var routeResults = new LinkedHashMap<String, String>();
|
||||||
|
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
|
||||||
|
Map<String, Endpoint> endpoints = new LinkedHashMap<>();
|
||||||
|
for (var site : sites) endpoints.put(site.id(), new Endpoint(site.id(), site.entry(), false));
|
||||||
|
for (var landmark : landmarks) if (!endpoints.containsKey(landmark.id()))
|
||||||
|
endpoints.put(landmark.id(), new Endpoint(landmark.id(), landmark.entry(), true));
|
||||||
|
var requests = new ArrayList<>(requests(sites, landmarks).stream()
|
||||||
|
.filter(r -> endpoints.containsKey(r.from()) && endpoints.containsKey(r.to())).toList());
|
||||||
|
var components = new Components(endpoints.keySet());
|
||||||
|
for (var link : links) components.join(link.fromId(), link.toId());
|
||||||
|
// Multiple exterior entries into the certified connected city represent one already walkable destination.
|
||||||
|
String firstCity = null;
|
||||||
|
for (var landmark : landmarks) if (landmark.kind() == LandmarkKind.CITY) {
|
||||||
|
if (firstCity == null) firstCity = landmark.id(); else components.join(firstCity, landmark.id());
|
||||||
|
}
|
||||||
|
Request longFreight = requests.stream().filter(r -> r.kind() == RouteKind.FREIGHT
|
||||||
|
&& span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()) >= diameter / 4)
|
||||||
|
.max(Comparator.comparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))).orElse(null);
|
||||||
|
var siteIds = new HashSet<String>(); sites.forEach(site -> siteIds.add(site.id()));
|
||||||
|
var addressed = new HashSet<String>();
|
||||||
|
var destinationAttempts = new HashMap<String, Integer>();
|
||||||
|
int attempted = 0;
|
||||||
|
while (!requests.isEmpty() && attempted < 36 && links.size() < 24) {
|
||||||
|
// Give each isolated site a first useful route before spending its columns on a cycle.
|
||||||
|
// Repeated city gates already belong to one walkable component; no redundant freight bypass is needed.
|
||||||
|
requests.removeIf(r -> components.same(r.from(), r.to()));
|
||||||
|
if (requests.isEmpty()) break;
|
||||||
|
requests.sort(Comparator.comparingInt((Request r) -> isolatedSites(r, siteIds, addressed) > 0
|
||||||
|
&& isolatedAttempts(r, siteIds, addressed, destinationAttempts) == 0 ? 0 : 1)
|
||||||
|
.thenComparingInt(r -> span(endpoints.get(r.from()).entry(), endpoints.get(r.to()).entry()))
|
||||||
|
.thenComparingInt(r -> r.kind() == RouteKind.FREIGHT ? 0 : 1).thenComparing(Request::id));
|
||||||
|
var request = attempted == 0 && longFreight != null ? longFreight : requests.getFirst(); requests.remove(request);
|
||||||
|
attempted++;
|
||||||
|
int distance = Math.abs(endpoints.get(request.from()).entry().x() - endpoints.get(request.to()).entry().x())
|
||||||
|
+ Math.abs(endpoints.get(request.from()).entry().z() - endpoints.get(request.to()).entry().z());
|
||||||
|
boolean longAxis = span(endpoints.get(request.from()).entry(), endpoints.get(request.to()).entry()) >= diameter / 4;
|
||||||
|
int isolated = isolatedSites(request, siteIds, addressed);
|
||||||
|
var waiting = new HashSet<String>();
|
||||||
|
for (var next : requests) for (var id : List.of(next.from(), next.to()))
|
||||||
|
if (siteIds.contains(id) && !addressed.contains(id) && !id.equals(request.from()) && !id.equals(request.to())) waiting.add(id);
|
||||||
|
int reserve = Math.min(Math.min(waiting.size(), 8), 36 - attempted) * 100;
|
||||||
|
int allowance = longAxis && attempted == 1 ? 3_800 : isolated > 0 ? Math.clamp(distance * 5 + 350, 900, 2_400) : 900;
|
||||||
|
probe.columnLimit = Math.min(MAX_COLUMNS - reserve, probe.columns.size() + allowance);
|
||||||
|
probe.sampleLimit = Math.min(MAX_SAMPLES - reserve * 20, probe.samples + (longAxis ? 150_000 : 80_000));
|
||||||
|
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||||
|
destinationAttempts.merge(id, 1, Integer::sum);
|
||||||
|
Endpoint from = endpoints.get(request.from()), to = endpoints.get(request.to());
|
||||||
|
int beforeRouteColumns = probe.columns.size();
|
||||||
|
var beforeReasons = new HashMap<>(routeReasons);
|
||||||
|
try {
|
||||||
|
RouteDraft route = route(seed, request, from, to, sites, landmarks, cells, probe);
|
||||||
|
if (route == null) { probe.reject("no-complete-route"); routeResults.put(request.id(), "refused; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); continue; }
|
||||||
|
var groups = new HashSet<SegmentKey>();
|
||||||
|
cells.values().forEach(c -> groups.add(new SegmentKey(c.request(), c.cell().x() >> 4, c.cell().z() >> 4)));
|
||||||
|
route.cells().values().stream().filter(c -> !cells.containsKey(key(c.x(), c.z())))
|
||||||
|
.forEach(c -> groups.add(new SegmentKey(request, c.x() >> 4, c.z() >> 4)));
|
||||||
|
if (groups.size() > 512) { routeResults.put(request.id(), "refused; native-piece-budget"); continue; }
|
||||||
|
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||||
|
components.join(request.from(), request.to());
|
||||||
|
addressed.add(request.from()); addressed.add(request.to());
|
||||||
|
routeResults.put(request.id(), "admitted; span=" + span(from.entry(), to.entry()) + "; length=" + route.path().size() + "; columns=" + (probe.columns.size() - beforeRouteColumns));
|
||||||
|
for (var cell : route.cells().values()) {
|
||||||
|
long key = key(cell.x(), cell.z());
|
||||||
|
OwnedCell old = cells.get(key);
|
||||||
|
if (old == null) cells.put(key, new OwnedCell(request, cell));
|
||||||
|
else if (cell.rail() != Rail.NONE && old.cell().rail() == Rail.NONE)
|
||||||
|
cells.put(key, new OwnedCell(old.request(), cell));
|
||||||
|
else if (cell.rail() != Rail.NONE && old.cell().rail() != Rail.NONE && cell.rail() != old.cell().rail()) {
|
||||||
|
// A missing switch is intentional; never emit a rail with contradictory connections.
|
||||||
|
var c = old.cell(); cells.put(key, new OwnedCell(old.request(),
|
||||||
|
new Cell(c.x(), c.floorY(), c.z(), c.foundationDepth(), c.rail(), false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { probe.reject(exhausted.columns ? "route-column-budget" : "route-probe-budget"); routeResults.put(request.id(), "budget; columns=" + (probe.columns.size() - beforeRouteColumns) + rejectionDelta(beforeReasons, routeReasons, probe.routeBlock)); }
|
||||||
|
}
|
||||||
|
for (var site : sites) {
|
||||||
|
var incident = links.stream().filter(l -> l.fromId().equals(site.id()) || l.toId().equals(site.id())).map(Link::id).toList();
|
||||||
|
var attempts = new LinkedHashMap<String, String>();
|
||||||
|
routeResults.forEach((id, value) -> { if (id.contains(site.id())) attempts.put(id, value); });
|
||||||
|
diagnostics.put(site.kind(), diagnostics.get(site.kind()) + "; connected=" + !incident.isEmpty()
|
||||||
|
+ "; accepted-links=" + incident + "; route-attempts=" + attempts);
|
||||||
|
}
|
||||||
|
var segments = segments(seed, cells);
|
||||||
|
if (Boolean.getBoolean("sanctuary.test.heritage22") || Boolean.getBoolean("sanctuary.heritage.plannerDiagnostics"))
|
||||||
|
System.getLogger(HeritagePlanner23.class.getName()).log(System.Logger.Level.INFO,
|
||||||
|
"Heritage23 seed=" + seed + " diameter=" + diameter + " sites=" + sites.stream().map(Site::kind).toList()
|
||||||
|
+ " links=" + links.size() + " segments=" + segments.size() + " columns=" + probe.columns.size()
|
||||||
|
+ " samples=" + probe.samples + " catalogue=" + diagnostics + " routes=" + routeReasons + " routeResults=" + routeResults);
|
||||||
|
return new HeritagePlan19(sites, landmarks, links, segments, probe.samples, probe.columns.size(), diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read-only natural stations: their admission never depends on a surviving building. */
|
||||||
|
private static void naturalRailEndpoints(long seed, int cx, int cz, int diameter,
|
||||||
|
List<Landmark> landmarks, Probe p) {
|
||||||
|
try {
|
||||||
|
double axis = unit(mix(seed ^ 0x23A115L)) * Math.PI * 2;
|
||||||
|
for (int end = 0; end < 2; end++) for (int attempt = 0; attempt < 16; attempt++) {
|
||||||
|
double angle = axis + end * Math.PI + (attempt % 2 == 0 ? 1 : -1) * (attempt / 2) * .07;
|
||||||
|
double radius = diameter * (attempt < 8 ? .25 : .18);
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius);
|
||||||
|
int z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
if (Math.hypot(x - cx, z - cz) < 56) continue;
|
||||||
|
int top = p.surface(x, z), facing = direction(x, z, cx, cz);
|
||||||
|
if (top < 16 || top > 345) continue;
|
||||||
|
boolean dry = true;
|
||||||
|
for (int dz = -2; dz <= 2 && dry; dz++) for (int dx = -2; dx <= 2; dx++) {
|
||||||
|
int h = p.surface(x + dx, z + dz);
|
||||||
|
if (h < top - 1 || h > top + 1 || !p.solid(x + dx, top - 2, z + dz)) { dry = false; break; }
|
||||||
|
for (int y = top - 2; y <= top + 3; y++)
|
||||||
|
if (p.protectedAt(x + dx, y, z + dz)) { dry = false; break; }
|
||||||
|
}
|
||||||
|
if (dry) { landmarks.add(new Landmark("natural_rail_" + end, LandmarkKind.MINE,
|
||||||
|
new Entry(x, top + 1, z, facing))); break; }
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { p.reject("natural-rail-budget"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A broken network may retain complete local railway runs even when a long connection is refused.
|
||||||
|
* Only the certified portion is published, with its actual natural stopping point. */
|
||||||
|
private static void naturalRailFragments(long seed, List<Landmark> landmarks, List<Site> sites,
|
||||||
|
List<Link> links, Map<Long, OwnedCell> cells, Probe p) {
|
||||||
|
p.columnLimit = MAX_COLUMNS - 4_000; p.sampleLimit = MAX_SAMPLES - 200_000;
|
||||||
|
List<Landmark> starts = landmarks.stream().filter(l -> l.id().startsWith("natural_rail_")).toList();
|
||||||
|
int index = 0;
|
||||||
|
try {
|
||||||
|
for (var original : starts) {
|
||||||
|
if (landmarks.size() >= 16) break;
|
||||||
|
boolean retained = false;
|
||||||
|
for (int turn = 0; turn < 4 && !retained; turn++) {
|
||||||
|
int face = (original.entry().facing() + turn) & 3;
|
||||||
|
var a = new Entry(original.entry().x(), original.entry().y(), original.entry().z(), face);
|
||||||
|
var from = new Endpoint(original.id(), a, true);
|
||||||
|
var oriented = new Landmark(original.id(), original.kind(), a);
|
||||||
|
var witnesses = new ArrayList<>(landmarks);
|
||||||
|
witnesses.set(witnesses.indexOf(original), oriented);
|
||||||
|
for (int length = 64; length >= 8; length -= 4) {
|
||||||
|
int x = a.x() + DX[face] * length, z = a.z() + DZ[face] * length;
|
||||||
|
int top = p.surface(x, z);
|
||||||
|
if (top < 14 || top > 350) continue;
|
||||||
|
String id = "rail_stop_" + index;
|
||||||
|
var end = new Landmark(id, LandmarkKind.MINE, new Entry(x, top + 1, z, (face + 2) & 3));
|
||||||
|
var candidateLandmarks = new ArrayList<>(witnesses); candidateLandmarks.add(end);
|
||||||
|
var points = new ArrayList<XZ>(); points.add(new XZ(a.x(), a.z()));
|
||||||
|
line(points, new XZ(x, z));
|
||||||
|
var request = new Request("fragment_" + index, original.id(), id, RouteKind.FREIGHT);
|
||||||
|
var route = profile(seed, request, from, new Endpoint(id, end.entry(), true),
|
||||||
|
points, sites, candidateLandmarks, cells, p);
|
||||||
|
if (route == null) continue;
|
||||||
|
landmarks.set(landmarks.indexOf(original), oriented); landmarks.add(end);
|
||||||
|
links.add(new Link(request.id(), request.from(), request.to(), request.kind(), route.path()));
|
||||||
|
for (var cell : route.cells().values()) cells.putIfAbsent(key(cell.x(), cell.z()), new OwnedCell(request, cell));
|
||||||
|
retained = true; index++; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { p.reject("fragment-budget"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
|
||||||
|
List<Site> sites, List<Landmark> landmarks, Probe probe) {
|
||||||
|
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
|
||||||
|
double radius = diameter * (kind == Kind.VOID_DOCK ? .40 : kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? .31 : .22);
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
Kind parent = switch (kind) {
|
||||||
|
case POST_OFFICE, COMMUNAL_DEPOT, RAIL_WORKSHOP -> Kind.FREIGHT_STATION;
|
||||||
|
case EXPEDITION_OFFICE, PHOTO_CLUB -> Kind.POST_OFFICE;
|
||||||
|
case SUBSTATION, CONSTRUCTION_REFUGE -> Kind.RAIL_WORKSHOP;
|
||||||
|
case GREENHOUSE, IMPOSSIBLE_SERVICE -> Kind.PUMP_STATION;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
Site neighbor = parent == null ? null : find(sites, parent);
|
||||||
|
if (neighbor != null) {
|
||||||
|
double local = unit(mix(seed ^ kind.ordinal() * 179L)) * Math.PI * 2;
|
||||||
|
int distance = kind == Kind.RAIL_WORKSHOP ? 65 : 40;
|
||||||
|
x = neighbor.centerX() + (int) Math.round(Math.cos(local) * distance);
|
||||||
|
z = neighbor.centerZ() + (int) Math.round(Math.sin(local) * distance);
|
||||||
|
}
|
||||||
|
var candidates = new ArrayList<Candidate>();
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
double turn = angle + i * Math.PI / 3;
|
||||||
|
int offset = i == 0 ? 0 : i < 5 ? 24 : i < 8 ? 48 : i < 12 ? 72 : i < 16 ? 96 : 128;
|
||||||
|
int px = x + (int) Math.round(Math.cos(turn) * offset), pz = z + (int) Math.round(Math.sin(turn) * offset);
|
||||||
|
if (Math.hypot((double) px - cx, (double) pz - cz) > diameter / 2.0 - 18) continue;
|
||||||
|
if (overlapsSites(px - 15, pz - 15, px + 15, pz + 15, sites)) continue;
|
||||||
|
int low = 384, high = -1; boolean dry = true;
|
||||||
|
for (int dz : new int[] {-8, 0, 8}) for (int dx : new int[] {-8, 0, 8}) {
|
||||||
|
int top = probe.surface(px + dx, pz + dz);
|
||||||
|
if (top < 48 || top > 330 || !probe.solid(px + dx, top, pz + dz)
|
||||||
|
|| probe.protectedAt(px + dx, top, pz + dz)) { dry = false; break; }
|
||||||
|
low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
}
|
||||||
|
if (!dry || high - low > MAX_SITE_RELIEF) { probe.reject("coarse-terrain"); continue; }
|
||||||
|
int facing = direction(px, pz, neighbor == null ? cx : neighbor.centerX(), neighbor == null ? cz : neighbor.centerZ());
|
||||||
|
if (kind == Kind.VOID_DOCK) facing = (direction(px, pz, cx, cz) + 2) & 3;
|
||||||
|
int score = (high - low) * 1_024 + i * 8 + (kind == Kind.OBSERVATORY || kind == Kind.RADIO_RELAY ? (300 - high) / 4 : 0);
|
||||||
|
candidates.add(new Candidate(px - 8, pz - 8, facing, score, mix(seed ^ kind.ordinal() * 463L ^ i)));
|
||||||
|
}
|
||||||
|
candidates.sort(Comparator.comparingInt(Candidate::score).thenComparingLong(Candidate::seed));
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SiteDraft site(long seed, Kind kind, Candidate candidate, List<Site> previous,
|
||||||
|
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int x = candidate.x(), z = candidate.z(), low = 384, high = -1;
|
||||||
|
for (var landmark : landmarks) if (landmark.entry().x() >= x - 2 && landmark.entry().x() <= x + 18
|
||||||
|
&& landmark.entry().z() >= z - 2 && landmark.entry().z() <= z + 18) { probe.reject("existing-entry"); return null; }
|
||||||
|
var heights = new ArrayList<Integer>();
|
||||||
|
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||||
|
int top = probe.surface(x + dx, z + dz);
|
||||||
|
if (top < 48 || top > 330 || !probe.solid(x + dx, top, z + dz)) { probe.reject("dry-footprint"); return null; }
|
||||||
|
heights.add(top); low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
}
|
||||||
|
int minimum = Math.max(48, high - MAX_SITE_CUT), maximum = Math.min(330, low + MAX_SITE_FILL);
|
||||||
|
if (minimum > maximum || high - low > MAX_SITE_RELIEF) { probe.reject("site-slope"); return null; }
|
||||||
|
heights.sort(Integer::compareTo);
|
||||||
|
int floor = Math.clamp(heights.get(heights.size() / 2), minimum, maximum);
|
||||||
|
int foundation = Math.max(3, floor - low + 1), height = height(kind);
|
||||||
|
int base = floor - foundation;
|
||||||
|
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++)
|
||||||
|
for (int y = base; y <= floor + height; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||||
|
probe.reject("site-protected-volume"); return null;
|
||||||
|
}
|
||||||
|
var supports = new ArrayList<Support>();
|
||||||
|
// Every foundation column rests directly on intact rock. Do not catch a cliff
|
||||||
|
// or a cavity with long piers just to retain this candidate.
|
||||||
|
for (int dz = 0; dz < 17; dz++) for (int dx = 0; dx < 17; dx++) {
|
||||||
|
if (!probe.solid(x + dx, base - 1, z + dz) || !probe.solid(x + dx, base - 2, z + dz)) {
|
||||||
|
probe.reject("site-no-direct-bearing"); return null;
|
||||||
|
}
|
||||||
|
for (int y = base - 2; y < base; y++) if (probe.protectedAt(x + dx, y, z + dz)) {
|
||||||
|
probe.reject("support-protected"); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The square envelope and bearing grid have already been certified. A difficult front slope
|
||||||
|
// should try another real facade before rejecting the entire building; no terrain is weakened.
|
||||||
|
for (int turn : new int[]{0, 1, 3, 2}) {
|
||||||
|
int facing = (candidate.facing() + turn) & 3;
|
||||||
|
int ex = facing == 1 ? x + 17 : facing == 3 ? x - 1 : x + 8;
|
||||||
|
int ez = facing == 2 ? z + 17 : facing == 0 ? z - 1 : z + 8;
|
||||||
|
Entry entry = new Entry(ex, floor + 1, ez, facing);
|
||||||
|
Site result = new Site(id(kind), kind, x, floor, z, 17, 17, height, facing,
|
||||||
|
mix(seed ^ candidate.seed()), foundation, supports, entry);
|
||||||
|
var landings = landing(result.entry(), previous, probe);
|
||||||
|
if (landings == null) { probe.reject("entry-landing"); continue; }
|
||||||
|
boolean collision = false;
|
||||||
|
for (var cell : landings) {
|
||||||
|
OwnedCell old = existing.get(key(cell.x(), cell.z()));
|
||||||
|
if (old != null && old.cell().floorY() != cell.floorY()) { collision = true; break; }
|
||||||
|
}
|
||||||
|
if (collision) { probe.reject("landing-collision"); continue; }
|
||||||
|
if (turn != 0) probe.reject("admitted-other-facade");
|
||||||
|
return new SiteDraft(result, landings);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int height(Kind kind) {
|
||||||
|
return switch (kind) {
|
||||||
|
case FREIGHT_STATION, POST_OFFICE, RAIL_WORKSHOP, SUBSTATION, GREENHOUSE, PHOTO_CLUB -> 10;
|
||||||
|
case EXPEDITION_OFFICE, IMPOSSIBLE_SERVICE -> 12;
|
||||||
|
case COMMUNAL_DEPOT, VOID_DOCK, CONSTRUCTION_REFUGE -> 14;
|
||||||
|
case PUMP_STATION -> 18; case OBSERVATORY -> 20; case RADIO_RELAY -> 22;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static List<Request> requests(List<Site> sites, List<Landmark> landmarks) {
|
||||||
|
var result = new LinkedHashMap<String, Request>();
|
||||||
|
var endpoints = new ArrayList<Endpoint>();
|
||||||
|
sites.forEach(s -> endpoints.add(new Endpoint(s.id(), s.entry(), false)));
|
||||||
|
landmarks.forEach(l -> endpoints.add(new Endpoint(l.id(), l.entry(), true)));
|
||||||
|
var freight = new HashSet<String>();
|
||||||
|
for (var kind : new Kind[] {Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT, Kind.RAIL_WORKSHOP, Kind.PUMP_STATION}) {
|
||||||
|
var site = find(sites, kind); if (site != null) freight.add(site.id());
|
||||||
|
}
|
||||||
|
landmarks.stream().filter(l -> l.kind() == LandmarkKind.MINE).forEach(l -> freight.add(l.id()));
|
||||||
|
for (var a : endpoints) {
|
||||||
|
// Four alternatives per entrance tolerate one protected cut without a quadratic terrain search.
|
||||||
|
var nearest = endpoints.stream().filter(b -> !a.id().equals(b.id()))
|
||||||
|
.sorted(Comparator.comparingInt((Endpoint b) -> span(a.entry(), b.entry())).thenComparing(Endpoint::id)).limit(4).toList();
|
||||||
|
for (var b : nearest) addRequest(result, a, b, freight.contains(a.id()) && freight.contains(b.id()) ? RouteKind.FREIGHT : RouteKind.PATH);
|
||||||
|
}
|
||||||
|
for (var pair : List.of(new Kind[]{Kind.FREIGHT_STATION, Kind.COMMUNAL_DEPOT},
|
||||||
|
new Kind[]{Kind.FREIGHT_STATION, Kind.RAIL_WORKSHOP}, new Kind[]{Kind.COMMUNAL_DEPOT, Kind.PUMP_STATION})) {
|
||||||
|
var a = find(sites, pair[0]); var b = find(sites, pair[1]);
|
||||||
|
if (a != null && b != null) addRequest(result, new Endpoint(a.id(), a.entry(), false), new Endpoint(b.id(), b.entry(), false), RouteKind.FREIGHT);
|
||||||
|
}
|
||||||
|
return List.copyOf(result.values());
|
||||||
|
}
|
||||||
|
private static void addRequest(Map<String, Request> result, Endpoint a, Endpoint b, RouteKind kind) {
|
||||||
|
if (a.id().compareTo(b.id()) > 0) { var swap = a; a = b; b = swap; }
|
||||||
|
String key = a.id() + "__" + b.id(); var old = result.get(key);
|
||||||
|
if (old == null || kind == RouteKind.FREIGHT) result.put(key, new Request("graph_" + key, a.id(), b.id(), kind));
|
||||||
|
}
|
||||||
|
private static String rejectionDelta(Map<String, Integer> before, Map<String, Integer> after, XZ obstacle) {
|
||||||
|
var changes = new java.util.TreeMap<String, Integer>();
|
||||||
|
after.forEach((key, count) -> { int difference = count - before.getOrDefault(key, 0); if (difference > 0) changes.put(key, difference); });
|
||||||
|
return "; reasons=" + changes + (obstacle == null ? "" : "; last-column=" + obstacle.x() + "," + obstacle.z());
|
||||||
|
}
|
||||||
|
private static int isolatedSites(Request request, Set<String> siteIds, Set<String> addressed) {
|
||||||
|
return (siteIds.contains(request.from()) && !addressed.contains(request.from()) ? 1 : 0)
|
||||||
|
+ (siteIds.contains(request.to()) && !addressed.contains(request.to()) ? 1 : 0);
|
||||||
|
}
|
||||||
|
private static int isolatedAttempts(Request request, Set<String> siteIds, Set<String> addressed, Map<String, Integer> attempts) {
|
||||||
|
int result = Integer.MAX_VALUE;
|
||||||
|
for (var id : List.of(request.from(), request.to())) if (siteIds.contains(id) && !addressed.contains(id))
|
||||||
|
result = Math.min(result, attempts.getOrDefault(id, 0));
|
||||||
|
return result == Integer.MAX_VALUE ? 0 : result;
|
||||||
|
}
|
||||||
|
private static final class Components {
|
||||||
|
final Map<String, String> parents = new HashMap<>();
|
||||||
|
Components(Set<String> ids) { for (var id : ids) parents.put(id, id); }
|
||||||
|
String root(String id) { String parent = parents.get(id); while (!parent.equals(id)) { id = parent; parent = parents.get(id); } return parent; }
|
||||||
|
boolean same(String a, String b) { return root(a).equals(root(b)); }
|
||||||
|
void join(String a, String b) { parents.put(root(b), root(a)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Always persisted, including for an isolated destination: five steps return to natural terrain. */
|
||||||
|
private static List<Cell> landing(Entry entry, List<Site> sites, Probe probe) {
|
||||||
|
int facing = entry.facing(), floor = entry.y() - 1;
|
||||||
|
int endX = entry.x() + DX[facing] * 4, endZ = entry.z() + DZ[facing] * 4;
|
||||||
|
int target = probe.surface(endX, endZ);
|
||||||
|
if (target < 14 || target > 350 || Math.abs(target - floor) > 3) return null;
|
||||||
|
var result = new ArrayList<Cell>();
|
||||||
|
for (int step = 0; step < 5; step++) {
|
||||||
|
// Keep the first two rows flat for the facade and the first rail turn.
|
||||||
|
int y = floor + (int) Math.round((target - floor) * Math.max(0, step - 1) / 3.0);
|
||||||
|
for (int cross = -1; cross <= 1; cross++) {
|
||||||
|
int x = entry.x() + DX[facing] * step + DZ[facing] * cross;
|
||||||
|
int z = entry.z() + DZ[facing] * step - DX[facing] * cross;
|
||||||
|
if (overlapsSites(x, z, x, z, sites)) return null;
|
||||||
|
Cell cell = routeCell(x, y, z, Rail.NONE, false, probe);
|
||||||
|
if (cell == null) return null;
|
||||||
|
result.add(cell);
|
||||||
|
if (step == 4) {
|
||||||
|
int beyond = probe.surface(x + DX[facing], z + DZ[facing]);
|
||||||
|
if (Math.abs(beyond - y) > 1 || !probe.solid(x + DX[facing], beyond, z + DZ[facing])) return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteDraft route(long seed, Request request, Endpoint from, Endpoint to, List<Site> sites,
|
||||||
|
List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
Entry a = from.entry(), b = to.entry();
|
||||||
|
XZ start = new XZ(a.x(), a.z()), end = new XZ(b.x(), b.z());
|
||||||
|
XZ outA = start.move(a.facing(), 6), outB = end.move(b.facing(), 6);
|
||||||
|
int middleX = (outA.x() + outB.x()) / 2, middleZ = (outA.z() + outB.z()) / 2;
|
||||||
|
var alternatives = List.of(
|
||||||
|
List.of(outA, new XZ(outB.x(), outA.z()), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), outB.z()), outB),
|
||||||
|
List.of(outA, new XZ(middleX, outA.z()), new XZ(middleX, outB.z()), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), middleZ), new XZ(outB.x(), middleZ), outB),
|
||||||
|
List.of(outA, new XZ(outA.x(), middleZ - 16), new XZ(outB.x(), middleZ - 16), outB),
|
||||||
|
List.of(outA, new XZ(middleX + 16, outA.z()), new XZ(middleX + 16, outB.z()), outB));
|
||||||
|
for (var waypoints : alternatives) {
|
||||||
|
var points = new ArrayList<XZ>(); points.add(start);
|
||||||
|
boolean okay = true;
|
||||||
|
for (var waypoint : waypoints) if (!line(points, waypoint)) { okay = false; break; }
|
||||||
|
if (!okay || !line(points, end) || points.size() > 1024 || new HashSet<>(points).size() != points.size()) continue;
|
||||||
|
RouteDraft result = profile(seed, request, from, to, points, sites, landmarks, existing, probe);
|
||||||
|
if (result != null) return result;
|
||||||
|
}
|
||||||
|
return adaptive(seed, request, from, to, start, end, outA, outB, sites, landmarks, existing, probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adaptive fallback. The coarse graph only proposes an alignment; it grants no writing permission. */
|
||||||
|
private static RouteDraft adaptive(long seed, Request request, Endpoint from, Endpoint to, XZ start, XZ end,
|
||||||
|
XZ outA, XZ outB, List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int stepSize = span(from.entry(), to.entry()) >= 160 ? 8 : 4;
|
||||||
|
int certificateReserve = Math.min(2_200, distance(outA, outB) * 3 + 80);
|
||||||
|
var excluded = new HashSet<XZ>();
|
||||||
|
for (int attempt = 0; attempt < 3; attempt++) {
|
||||||
|
var open = new java.util.PriorityQueue<SearchNode>(Comparator.comparingDouble(SearchNode::priority)
|
||||||
|
.thenComparingDouble(SearchNode::cost).thenComparingInt(n -> n.position().x()).thenComparingInt(n -> n.position().z()));
|
||||||
|
var best = new HashMap<XZ, Double>(); var parents = new HashMap<XZ, XZ>(); var closed = new HashSet<XZ>();
|
||||||
|
var terrainHeights = new HashMap<XZ, Integer>();
|
||||||
|
int begin = coarseRoute(outA, sites, terrainHeights, probe);
|
||||||
|
if (begin == INVALID) return null;
|
||||||
|
open.add(new SearchNode(outA, 0, distance(outA, outB))); best.put(outA, 0.0);
|
||||||
|
List<XZ> found = null;
|
||||||
|
int searchColumns = Math.max(probe.columns.size() + 150, probe.columnLimit - certificateReserve);
|
||||||
|
while (!open.isEmpty() && closed.size() < 500 && probe.columns.size() < searchColumns) {
|
||||||
|
var current = open.remove(); XZ p = current.position();
|
||||||
|
if (!closed.add(p) || current.cost() > best.getOrDefault(p, Double.POSITIVE_INFINITY)) continue;
|
||||||
|
if (distance(p, outB) <= stepSize * 2) {
|
||||||
|
var reversed = new ArrayList<XZ>(); XZ ancestor = p;
|
||||||
|
while (ancestor != null) { reversed.add(ancestor); ancestor = parents.get(ancestor); }
|
||||||
|
java.util.Collections.reverse(reversed);
|
||||||
|
for (int order = 0; order < 2 && found == null; order++) {
|
||||||
|
var candidate = new ArrayList<XZ>(); candidate.add(start); boolean valid = true;
|
||||||
|
for (XZ step : reversed) if (!line(candidate, step)) { valid = false; break; }
|
||||||
|
XZ turn = order == 0 ? new XZ(outB.x(), p.z()) : new XZ(p.x(), outB.z());
|
||||||
|
if (valid && line(candidate, turn) && line(candidate, outB) && line(candidate, end)
|
||||||
|
&& new HashSet<>(candidate).size() == candidate.size()) found = candidate;
|
||||||
|
}
|
||||||
|
if (found != null) break;
|
||||||
|
}
|
||||||
|
int top = terrainHeights.get(p); XZ parent = parents.get(p);
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
if (parent == null && d == ((from.entry().facing() + 2) & 3)) continue;
|
||||||
|
XZ next = p.move(d, stepSize);
|
||||||
|
if (closed.contains(next) || excluded.contains(next)
|
||||||
|
|| Math.min(distance(next, outA), distance(next, outB)) > distance(outA, outB) + 64) continue;
|
||||||
|
int height = coarseRoute(next, sites, terrainHeights, probe);
|
||||||
|
if (height == INVALID || Math.abs(height - top) > stepSize + 3) continue;
|
||||||
|
double turnCost = parent == null || parent.x() == next.x() || parent.z() == next.z() ? 0 : 1.5;
|
||||||
|
double cost = current.cost() + stepSize + Math.abs(height - top) * 1.1 + turnCost;
|
||||||
|
if (cost >= best.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
|
||||||
|
best.put(next, cost); parents.put(next, p);
|
||||||
|
open.add(new SearchNode(next, cost, cost + distance(next, outB) * 1.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == null) { probe.reject("adaptive-no-alignment"); return null; }
|
||||||
|
RouteDraft certified = profile(seed, request, from, to, found, sites, landmarks, existing, probe);
|
||||||
|
if (certified != null) { probe.reject("adaptive-admitted"); return certified; }
|
||||||
|
if (probe.routeBlock == null) return null;
|
||||||
|
// A failed exact corridor removes only nearby coarse nodes on the next proposal.
|
||||||
|
XZ bad = probe.routeBlock;
|
||||||
|
int gx = outA.x() + (int) Math.round((bad.x() - outA.x()) / (double) stepSize) * stepSize;
|
||||||
|
int gz = outA.z() + (int) Math.round((bad.z() - outA.z()) / (double) stepSize) * stepSize;
|
||||||
|
excluded.add(new XZ(gx, gz));
|
||||||
|
for (int d = 0; d < 4; d++) excluded.add(new XZ(gx, gz).move(d, stepSize));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
private record SearchNode(XZ position, double cost, double priority) {}
|
||||||
|
private static int distance(XZ a, XZ b) { return Math.abs(a.x() - b.x()) + Math.abs(a.z() - b.z()); }
|
||||||
|
private static int coarseRoute(XZ p, List<Site> sites, Map<XZ, Integer> cache, Probe probe) {
|
||||||
|
Integer old = cache.get(p); if (old != null) return old;
|
||||||
|
if (overlapsSites(p.x() - 2, p.z() - 2, p.x() + 2, p.z() + 2, sites)) { cache.put(p, INVALID); return INVALID; }
|
||||||
|
int top = probe.surface(p.x(), p.z());
|
||||||
|
if (top < 14 || top > 350 || !probe.solid(p.x(), top, p.z()) || probe.protectedAt(p.x(), top, p.z())) {
|
||||||
|
cache.put(p, INVALID); return INVALID;
|
||||||
|
}
|
||||||
|
for (int d = 0; d < 4; d++) {
|
||||||
|
XZ side = p.move(d, 1); int sideTop = probe.surface(side.x(), side.z());
|
||||||
|
if (sideTop < 14 || sideTop > 350 || Math.abs(sideTop - top) > MAX_ROUTE_CUT + 1 || probe.protectedAt(side.x(), sideTop, side.z())) {
|
||||||
|
cache.put(p, INVALID); return INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.put(p, top); return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finite orthogonal alignments, each certified at full block resolution before publication. */
|
||||||
|
private static boolean line(List<XZ> points, XZ destination) {
|
||||||
|
XZ p = points.getLast();
|
||||||
|
if (p.x() != destination.x() && p.z() != destination.z()) return false;
|
||||||
|
int dx = Integer.signum(destination.x() - p.x()), dz = Integer.signum(destination.z() - p.z());
|
||||||
|
while (!p.equals(destination)) {
|
||||||
|
p = new XZ(p.x() + dx, p.z() + dz); points.add(p);
|
||||||
|
if (points.size() > 1024) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteDraft profile(long seed, Request request, Endpoint from, Endpoint to, List<XZ> points,
|
||||||
|
List<Site> sites, List<Landmark> landmarks, Map<Long, OwnedCell> existing, Probe probe) {
|
||||||
|
int size = points.size(); probe.routeBlock = null;
|
||||||
|
int[] lo = new int[size], hi = new int[size], desired = new int[size], group = new int[size];
|
||||||
|
boolean[] flat = new boolean[size - 1];
|
||||||
|
for (int i = 1; i < size - 1; i++) if (turn(points, i)) { flat[i - 1] = true; flat[i] = true; }
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
XZ point = points.get(i); probe.routeBlock = point; lo[i] = 14; hi[i] = 350;
|
||||||
|
desired[i] = probe.surface(point.x(), point.z());
|
||||||
|
boolean readOnly = i == 0 && from.readOnly() || i == size - 1 && to.readOnly();
|
||||||
|
for (XZ column : strip(points, i)) {
|
||||||
|
if (overlapsSites(column.x(), column.z(), column.x(), column.z(), sites)) { probe.reject("route-building"); return null; }
|
||||||
|
int top = probe.surface(column.x(), column.z());
|
||||||
|
if (top < 14 || top > 350 || !probe.solid(column.x(), top, column.z())) { probe.reject("route-dry-ground"); return null; }
|
||||||
|
lo[i] = Math.max(lo[i], top - MAX_ROUTE_CUT); hi[i] = Math.min(hi[i], top + MAX_ROUTE_FILL);
|
||||||
|
OwnedCell old = existing.get(key(column.x(), column.z()));
|
||||||
|
if (old != null) { lo[i] = Math.max(lo[i], old.cell().floorY()); hi[i] = Math.min(hi[i], old.cell().floorY()); }
|
||||||
|
if (readOnly && top != (i == 0 ? from.entry().y() : to.entry().y()) - 1) return null;
|
||||||
|
}
|
||||||
|
if (i == 0 || i == size - 1) {
|
||||||
|
int exact = (i == 0 ? from.entry().y() : to.entry().y()) - 1;
|
||||||
|
lo[i] = Math.max(lo[i], exact); hi[i] = Math.min(hi[i], exact);
|
||||||
|
}
|
||||||
|
if (lo[i] > hi[i]) { probe.reject("route-grade-interval"); return null; }
|
||||||
|
}
|
||||||
|
// Curves and both adjacent rails share one elevation; a Minecraft curved rail cannot slope.
|
||||||
|
int groups = 1;
|
||||||
|
for (int i = 1; i < size; i++) { if (!flat[i - 1]) groups++; group[i] = groups - 1; }
|
||||||
|
int[] gl = new int[groups], gh = new int[groups], gd = new int[groups], y = new int[groups];
|
||||||
|
java.util.Arrays.fill(gl, 14); java.util.Arrays.fill(gh, 350);
|
||||||
|
for (int i = 0; i < size; i++) { int g = group[i]; gl[g] = Math.max(gl[g], lo[i]); gh[g] = Math.min(gh[g], hi[i]); gd[g] = desired[i]; }
|
||||||
|
for (int g = 1; g < groups; g++) { gl[g] = Math.max(gl[g], gl[g - 1] - 1); gh[g] = Math.min(gh[g], gh[g - 1] + 1); }
|
||||||
|
for (int g = groups - 2; g >= 0; g--) { gl[g] = Math.max(gl[g], gl[g + 1] - 1); gh[g] = Math.min(gh[g], gh[g + 1] + 1); }
|
||||||
|
for (int g = 0; g < groups; g++) {
|
||||||
|
if (gl[g] > gh[g]) { probe.reject("route-grade"); return null; }
|
||||||
|
int lower = g == 0 ? gl[g] : Math.max(gl[g], y[g - 1] - 1), upper = g == 0 ? gh[g] : Math.min(gh[g], y[g - 1] + 1);
|
||||||
|
y[g] = Math.clamp(gd[g], lower, upper);
|
||||||
|
}
|
||||||
|
var path = new ArrayList<Point>(); var floors = new LinkedHashMap<Long, Integer>();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
XZ point = points.get(i); int floor = y[group[i]]; path.add(new Point(point.x(), floor, point.z()));
|
||||||
|
if (i == 0 && from.readOnly() || i == size - 1 && to.readOnly()) continue;
|
||||||
|
for (XZ column : strip(points, i)) {
|
||||||
|
long key = key(column.x(), column.z()); Integer old = floors.putIfAbsent(key, floor);
|
||||||
|
if (old != null && old != floor) { probe.reject("route-self-grade"); return null; }
|
||||||
|
// Historical endpoints and their lateral landing are read-only, even at another nearby turn.
|
||||||
|
for (var landmark : landmarks) if (landingColumn(landmark.entry(), column)) {
|
||||||
|
boolean actualEnd = from.readOnly() && landmark.id().equals(from.id())
|
||||||
|
|| to.readOnly() && landmark.id().equals(to.id());
|
||||||
|
if (!actualEnd || floor != landmark.entry().y() - 1) { probe.reject("route-historical-entry"); return null; }
|
||||||
|
floors.remove(key); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var cells = new LinkedHashMap<Long, Cell>();
|
||||||
|
for (var entry : floors.entrySet()) {
|
||||||
|
int x = (int) (entry.getKey() >> 32), z = (int) (long) entry.getKey();
|
||||||
|
probe.routeBlock = new XZ(x, z);
|
||||||
|
Cell cell = routeCell(x, entry.getValue(), z, Rail.NONE, false, probe);
|
||||||
|
if (cell == null) { probe.reject("route-column-certificate"); return null; }
|
||||||
|
cells.put(entry.getKey(), cell);
|
||||||
|
}
|
||||||
|
if (request.kind() == RouteKind.FREIGHT) for (int i = 0; i < path.size(); i++) {
|
||||||
|
Point point = path.get(i); long key = key(point.x(), point.z()); Cell cell = cells.get(key);
|
||||||
|
if (cell == null) continue;
|
||||||
|
Rail rail = rail(path, i);
|
||||||
|
// A one-block valley cannot carry two ascending slopes on the same rail. Keep a repairable rail gap.
|
||||||
|
boolean intact = rail != Rail.NONE && Math.floorMod(mix(seed ^ key ^ request.id().hashCode()), 19) != 0;
|
||||||
|
if (rail == Rail.NONE) rail = points.get(Math.max(0, i - 1)).x() == points.get(Math.min(size - 1, i + 1)).x() ? Rail.NORTH_SOUTH : Rail.EAST_WEST;
|
||||||
|
cells.put(key, new Cell(cell.x(), cell.floorY(), cell.z(), cell.foundationDepth(), rail, intact));
|
||||||
|
}
|
||||||
|
return new RouteDraft(List.copyOf(path), cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean landingColumn(Entry entry, XZ column) {
|
||||||
|
int dx = column.x() - entry.x(), dz = column.z() - entry.z();
|
||||||
|
return DX[entry.facing()] * dx + DZ[entry.facing()] * dz == 0 && Math.abs(dx) + Math.abs(dz) <= 1;
|
||||||
|
}
|
||||||
|
private static List<XZ> strip(List<XZ> points, int i) {
|
||||||
|
XZ p = points.get(i); var result = new ArrayList<XZ>();
|
||||||
|
if (turn(points, i)) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) result.add(new XZ(p.x() + dx, p.z() + dz));
|
||||||
|
} else {
|
||||||
|
XZ other = points.get(i == points.size() - 1 ? i - 1 : i + 1);
|
||||||
|
int dx = Integer.signum(other.x() - p.x()), dz = Integer.signum(other.z() - p.z());
|
||||||
|
for (int cross = -1; cross <= 1; cross++) result.add(new XZ(p.x() + dz * cross, p.z() - dx * cross));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static boolean turn(List<XZ> points, int i) {
|
||||||
|
if (i == 0 || i == points.size() - 1) return false;
|
||||||
|
XZ a = points.get(i - 1), b = points.get(i + 1);
|
||||||
|
return a.x() != b.x() && a.z() != b.z();
|
||||||
|
}
|
||||||
|
private static Rail rail(List<Point> path, int i) {
|
||||||
|
Point p = path.get(i), a = path.get(Math.max(0, i - 1)), b = path.get(Math.min(path.size() - 1, i + 1));
|
||||||
|
if (a.floorY() > p.floorY() && b.floorY() > p.floorY()) return Rail.NONE;
|
||||||
|
Point high = a.floorY() > p.floorY() ? a : b.floorY() > p.floorY() ? b : null;
|
||||||
|
if (high != null) return switch (direction(p.x(), p.z(), high.x(), high.z())) {
|
||||||
|
case 0 -> Rail.ASCENDING_NORTH; case 1 -> Rail.ASCENDING_EAST; case 2 -> Rail.ASCENDING_SOUTH; default -> Rail.ASCENDING_WEST;
|
||||||
|
};
|
||||||
|
if (a.x() == b.x()) return Rail.NORTH_SOUTH;
|
||||||
|
if (a.z() == b.z()) return Rail.EAST_WEST;
|
||||||
|
boolean n = a.z() < p.z() || b.z() < p.z(), s = a.z() > p.z() || b.z() > p.z();
|
||||||
|
boolean e = a.x() > p.x() || b.x() > p.x();
|
||||||
|
return n ? e ? Rail.NORTH_EAST : Rail.NORTH_WEST : s && e ? Rail.SOUTH_EAST : Rail.SOUTH_WEST;
|
||||||
|
}
|
||||||
|
private static Cell routeCell(int x, int floor, int z, Rail rail, boolean present, Probe probe) {
|
||||||
|
int top = probe.surface(x, z);
|
||||||
|
if (floor < 14 || floor > 350 || top < floor - MAX_ROUTE_FILL || top > floor + MAX_ROUTE_CUT) return null;
|
||||||
|
int foundation = Math.max(1, floor - top + 1), base = floor - foundation;
|
||||||
|
if (!probe.solid(x, base - 1, z) || !probe.solid(x, base, z)) return null;
|
||||||
|
for (int y = base - 1; y <= floor + 3; y++) if (probe.protectedAt(x, y, z)) return null;
|
||||||
|
return new Cell(x, floor, z, foundation, rail, present);
|
||||||
|
}
|
||||||
|
private record XZ(int x, int z) { XZ move(int facing, int distance) { return new XZ(x + DX[facing] * distance, z + DZ[facing] * distance); } }
|
||||||
|
private record SegmentKey(Request request, int cx, int cz) {}
|
||||||
|
private static List<Segment> segments(long seed, Map<Long, OwnedCell> cells) {
|
||||||
|
var groups = new LinkedHashMap<SegmentKey, List<Cell>>();
|
||||||
|
for (var owned : cells.values()) {
|
||||||
|
Cell c = owned.cell(); var key = new SegmentKey(owned.request(), c.x() >> 4, c.z() >> 4);
|
||||||
|
groups.computeIfAbsent(key, ignored -> new ArrayList<>()).add(c);
|
||||||
|
}
|
||||||
|
var result = new ArrayList<Segment>();
|
||||||
|
for (var entry : groups.entrySet()) {
|
||||||
|
var k = entry.getKey(); var r = k.request();
|
||||||
|
// IDs are stable, bounded and independent of native generation order.
|
||||||
|
String id = "piece_" + r.id() + "_" + k.cx() + "_" + k.cz();
|
||||||
|
result.add(new Segment(id, r.from(), r.to(), r.kind(), k.cx(), k.cz(), entry.getValue(), mix(seed ^ key(k.cx(), k.cz()) ^ r.id().hashCode())));
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int span(Entry a, Entry b) { return Math.max(Math.abs(a.x() - b.x()), Math.abs(a.z() - b.z())); }
|
||||||
|
private static Site find(List<Site> sites, Kind kind) { return sites.stream().filter(site -> site.kind() == kind).findFirst().orElse(null); }
|
||||||
|
private static String id(Kind kind) { return kind.name().toLowerCase(Locale.ROOT); }
|
||||||
|
private static int direction(int x, int z, int tx, int tz) { return Math.abs(tx - x) >= Math.abs(tz - z) ? tx > x ? 1 : 3 : tz > z ? 2 : 0; }
|
||||||
|
private static boolean overlapsSites(int x0, int z0, int x1, int z1, List<Site> sites) {
|
||||||
|
for (var site : sites) if (x0 <= site.maxX() && x1 >= site.minX() && z0 <= site.maxZ() && z1 >= site.minZ()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private record SiteDraft(Site site, List<Cell> landings) {}
|
||||||
|
private record Candidate(int x, int z, int facing, int score, long seed) {}
|
||||||
|
private record Endpoint(String id, Entry entry, boolean readOnly) {}
|
||||||
|
private record Request(String id, String from, String to, RouteKind kind) {}
|
||||||
|
private record OwnedCell(Request request, Cell cell) {}
|
||||||
|
private record RouteDraft(List<Point> path, Map<Long, Cell> cells) {}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException {
|
||||||
|
final boolean columns; BudgetExceeded(boolean columns) { this.columns = columns; }
|
||||||
|
}
|
||||||
|
private static final class Probe {
|
||||||
|
final Terrain terrain; final int cx, cz; final double radius;
|
||||||
|
final Map<Long, Integer> columns = new HashMap<>();
|
||||||
|
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
|
||||||
|
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
|
||||||
|
int surface(int x, int z) {
|
||||||
|
if (Math.hypot(x - cx, z - cz) < 48) return INVALID;
|
||||||
|
if (Math.hypot((double) x - cx, (double) z - cz) > radius || !terrain.owns(x, z)) return INVALID;
|
||||||
|
long key = key(x, z); var cached = columns.get(key); if (cached != null) return cached;
|
||||||
|
if (columns.size() >= columnLimit) throw new BudgetExceeded(true);
|
||||||
|
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||||
|
}
|
||||||
|
boolean solid(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return false; count(); return terrain.solid(x, y, z); }
|
||||||
|
boolean protectedAt(int x, int y, int z) { if (y < 1 || y > 383 || surface(x, z) == INVALID) return true; count(); return terrain.protectedAt(x, y, z); }
|
||||||
|
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(false); samples++; }
|
||||||
|
void reject(String reason) { reasons.merge(reason, 1, Integer::sum); }
|
||||||
|
}
|
||||||
|
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||||
|
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||||
|
private static long mix(long value) { value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L; value = (value ^ value >>> 27) * 0x94d049bb133111ebL; return value ^ value >>> 31; }
|
||||||
|
}
|
||||||
+3
-3
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
|
|||||||
|
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
||||||
var plan = generator.heritagePlan19();
|
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.
|
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||||
for (var segment : plan.segments())
|
for (var segment : plan.segments())
|
||||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||||
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment));
|
||||||
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
for (var site : plan.sites()) if (site.startChunkX() == x && site.startChunkZ() == z)
|
||||||
pieces.add((generator.rootGeneration() == 21 || generator.rootGeneration() == 22) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site));
|
||||||
if (pieces.isEmpty()) return Optional.empty();
|
if (pieces.isEmpty()) return Optional.empty();
|
||||||
for (var piece : pieces) {
|
for (var piece : pieces) {
|
||||||
var box = piece.getBoundingBox();
|
var box = piece.getBoundingBox();
|
||||||
|
|||||||
+2
-2
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||||
|| !sanctuary.experimentalStructures()) return;
|
|| !sanctuary.experimentalStructures()) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||||
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()
|
||||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.IntTag;
|
||||||
|
import net.minecraft.nbt.LongTag;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
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.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
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;
|
||||||
|
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) {
|
||||||
|
super(MineralSanctuaryStructures23.PIECE, 0, chamber.bounds());
|
||||||
|
this.chamber = chamber; setOrientation(null);
|
||||||
|
}
|
||||||
|
public MineralSanctuaryPiece23(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
super(MineralSanctuaryStructures23.PIECE, tag);
|
||||||
|
chamber = decode(tag.getCompound("SanctuaryMineral").orElseThrow());
|
||||||
|
if (!getBoundingBox().equals(chamber.bounds())) throw new IllegalArgumentException("Mineral piece bounding box mismatch");
|
||||||
|
setOrientation(null);
|
||||||
|
}
|
||||||
|
public MineralSanctuaryPlan23.Chamber chamber() { return chamber; }
|
||||||
|
public static CompoundTag encode(MineralSanctuaryPlan23.Chamber c) {
|
||||||
|
var tag = new CompoundTag(); tag.putInt("Schema", 1);
|
||||||
|
tag.putInt("X", c.minX()); tag.putInt("Y", c.minY()); tag.putInt("Z", c.minZ());
|
||||||
|
tag.putInt("Width", c.width()); tag.putInt("Height", c.height()); tag.putInt("Depth", c.depth()); tag.putInt("Facing", c.facing()); tag.putLong("Seed", c.seed());
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
public static MineralSanctuaryPlan23.Chamber decode(CompoundTag tag) {
|
||||||
|
if (!tag.keySet().equals(Set.of("Schema", "X", "Y", "Z", "Width", "Height", "Depth", "Facing", "Seed")))
|
||||||
|
throw new IllegalArgumentException("Unexpected mineral piece fields");
|
||||||
|
for (String key : new String[]{"Schema", "X", "Y", "Z", "Width", "Height", "Depth", "Facing"})
|
||||||
|
if (!(tag.get(key) instanceof IntTag)) throw new IllegalArgumentException("Expected integer mineral field: " + key);
|
||||||
|
if (!(tag.get("Seed") instanceof LongTag)) throw new IllegalArgumentException("Expected long mineral seed");
|
||||||
|
if (tag.getInt("Schema").orElse(-1) != 1) throw new IllegalArgumentException("Unsupported mineral piece schema");
|
||||||
|
return new MineralSanctuaryPlan23.Chamber(tag.getInt("X").orElseThrow(), tag.getInt("Y").orElseThrow(),
|
||||||
|
tag.getInt("Z").orElseThrow(), tag.getInt("Width").orElseThrow(), tag.getInt("Height").orElseThrow(),
|
||||||
|
tag.getInt("Depth").orElseThrow(), tag.getInt("Facing").orElseThrow(), tag.getLong("Seed").orElseThrow());
|
||||||
|
}
|
||||||
|
@Override public BlockPos getLocatorPosition() { return chamber.locator(); }
|
||||||
|
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||||
|
tag.put("SanctuaryMineral", encode(chamber));
|
||||||
|
}
|
||||||
|
@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() {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
@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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
|
||||||
|
/** Immutable geometry saved in one native piece. No world or registry is retained by the plan. */
|
||||||
|
public record MineralSanctuaryPlan23(Chamber chamber, int samples, int columns, Map<String, Integer> rejections) {
|
||||||
|
public static final MineralSanctuaryPlan23 EMPTY = new MineralSanctuaryPlan23(null, 0, 0);
|
||||||
|
public MineralSanctuaryPlan23(Chamber chamber, int samples, int columns) { this(chamber, samples, columns, Map.of()); }
|
||||||
|
public MineralSanctuaryPlan23 {
|
||||||
|
rejections = Map.copyOf(rejections);
|
||||||
|
if (samples < 0 || samples > MineralSanctuaryPlanner23.MAX_SAMPLES || columns < 0
|
||||||
|
|| columns > MineralSanctuaryPlanner23.MAX_COLUMNS) throw new IllegalArgumentException("Invalid mineral budget");
|
||||||
|
}
|
||||||
|
public Map<String, Integer> diagnostic() { return rejections; }
|
||||||
|
public boolean isEmpty() { return chamber == null; }
|
||||||
|
public boolean protects(int x, int y, int z) {
|
||||||
|
return chamber != null && x >= chamber.minX() && x <= chamber.maxX()
|
||||||
|
&& z >= chamber.minZ() && z <= chamber.maxZ() && y >= chamber.minY() - 2 && y <= chamber.maxY() + 2;
|
||||||
|
}
|
||||||
|
public boolean contains(int x, int y, int z) { return protects(x, y, z); }
|
||||||
|
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||||
|
return chamber != null && x0 <= chamber.maxX() && x1 >= chamber.minX()
|
||||||
|
&& z0 <= chamber.maxZ() && z1 >= chamber.minZ() && y0 <= chamber.maxY() + 2 && y1 >= chamber.minY() - 2;
|
||||||
|
}
|
||||||
|
public BoundingBox bounds() { return chamber == null ? null : chamber.bounds(); }
|
||||||
|
public record Chamber(int minX, int minY, int minZ, int width, int height, int depth, int facing, long seed) {
|
||||||
|
public Chamber {
|
||||||
|
if (width != 79 || depth != 73 || height != 27 || facing < 0 || facing > 3
|
||||||
|
|| Math.abs((long) minX) > 29_999_000 || Math.abs((long) minZ) > 29_999_000
|
||||||
|
|| width % 2 == 0 || depth % 2 == 0 || minY < 8 || minY + height > 376)
|
||||||
|
throw new IllegalArgumentException("Invalid mineral sanctuary geometry");
|
||||||
|
}
|
||||||
|
public int maxX() { return minX + width - 1; }
|
||||||
|
public int maxY() { return minY + height - 1; }
|
||||||
|
public int maxZ() { return minZ + depth - 1; }
|
||||||
|
public int centerX() { return minX + width / 2; }
|
||||||
|
public int centerZ() { return minZ + depth / 2; }
|
||||||
|
public int startChunkX() { return centerX() >> 4; }
|
||||||
|
public int startChunkZ() { return centerZ() >> 4; }
|
||||||
|
public BoundingBox bounds() { return new BoundingBox(minX, minY, minZ, maxX(), maxY(), maxZ()); }
|
||||||
|
public BlockPos locator() { return new BlockPos(centerX(), minY + 4, centerZ()); }
|
||||||
|
/** A lobed rock envelope, not a rectangular excavated room. Coordinates are local. */
|
||||||
|
public double radius(int x, int z) {
|
||||||
|
double nx = (x - width / 2.0 + .5) / (width / 2.0 - 1);
|
||||||
|
double nz = (z - depth / 2.0 + .5) / (depth / 2.0 - 1);
|
||||||
|
double angle = Math.atan2(nz, nx), phase = (seed >>> 12 & 1023) / 1024.0 * Math.PI * 2;
|
||||||
|
return Math.hypot(nx, nz) / (.88 + .065 * Math.sin(angle * 3 + phase) + .035 * Math.cos(angle * 7 - phase));
|
||||||
|
}
|
||||||
|
public boolean footprint(int x, int z) { return radius(x, z) <= 1; }
|
||||||
|
/** Low terraces form walkable natural steps around a clear central floor. */
|
||||||
|
public int floor(int x, int z) {
|
||||||
|
if (centralFloor(x, z)) return 2;
|
||||||
|
int distance = Math.max(Math.abs(x - width / 2), Math.abs(z - depth / 2)) - 19;
|
||||||
|
return 2 + Math.min(distance, (int) Math.floor(Math.max(0, radius(x, z) - .40) * 7));
|
||||||
|
}
|
||||||
|
public int ceiling(int x, int z) {
|
||||||
|
double r = Math.min(1, radius(x, z));
|
||||||
|
return Math.max(floor(x, z) + 5, (int) Math.round(height - 4 - r * r * (height - 14)));
|
||||||
|
}
|
||||||
|
public boolean pillar(int x, int z) {
|
||||||
|
int dx = Math.floorMod(x - width / 2 + 7, 17) - 8;
|
||||||
|
int dz = Math.floorMod(z - depth / 2 + 5, 15) - 7;
|
||||||
|
return !centralFloor(x, z) && radius(x, z) < .85 && dx * dx + dz * dz <= 3;
|
||||||
|
}
|
||||||
|
public boolean centralFloor(int x, int z) { return Math.abs(x - width / 2) <= 19 && Math.abs(z - depth / 2) <= 19; }
|
||||||
|
public boolean accessFootprint(int x, int z) {
|
||||||
|
return switch (facing) {
|
||||||
|
case 0 -> z <= depth / 2 && Math.abs(x - width / 2) <= 4;
|
||||||
|
case 1 -> x >= width / 2 && Math.abs(z - depth / 2) <= 4;
|
||||||
|
case 2 -> z >= depth / 2 && Math.abs(x - width / 2) <= 4;
|
||||||
|
default -> x <= width / 2 && Math.abs(z - depth / 2) <= 4;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public boolean portal(int x, int y, int z) {
|
||||||
|
int axis = facing % 2 == 0 ? z : x;
|
||||||
|
int lateral = facing % 2 == 0 ? x - width / 2 : z - depth / 2;
|
||||||
|
int bend = (int) Math.round(Math.sin(axis * .13 + (seed & 7)) * 2);
|
||||||
|
return accessFootprint(x, z) && Math.abs(lateral - bend) <= 2
|
||||||
|
&& y > floor(x, z) && y <= floor(x, z) + 5;
|
||||||
|
}
|
||||||
|
public boolean writes(int x, int y, int z) {
|
||||||
|
return footprint(x, z) || portal(x, y, z) || accessFootprint(x, z) && y <= floor(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Finite terrain search: at most48 horizontal candidates, four orientations, three depths. */
|
||||||
|
public final class MineralSanctuaryPlanner23 {
|
||||||
|
public static final int MAX_COLUMNS = 10_000, MAX_SAMPLES = 1_200_000;
|
||||||
|
private MineralSanctuaryPlanner23() {}
|
||||||
|
public interface Terrain {
|
||||||
|
boolean owns(int x, int z);
|
||||||
|
int surfaceY(int x, int z);
|
||||||
|
boolean solid(int x, int y, int z);
|
||||||
|
boolean protectedAt(int x, int y, int z);
|
||||||
|
}
|
||||||
|
public static MineralSanctuaryPlan23 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
|
||||||
|
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported mineral island size");
|
||||||
|
var p = new Probe(terrain);
|
||||||
|
try {
|
||||||
|
for (int attempt = 0; attempt < 48; attempt++) {
|
||||||
|
double angle = unit(mix(seed ^ 0x23CA7EL)) * Math.PI * 2 + attempt * 2.399963229728653;
|
||||||
|
double radius = diameter * (.17 + .045 * (attempt % 4));
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
int surface = p.surface(x, z);
|
||||||
|
if (surface < 80) { p.reject("candidate-surface"); continue; }
|
||||||
|
for (int drop : new int[]{46, 78, 110}) for (int facing = 0; facing < 4; facing++) {
|
||||||
|
int width = 79, depth = 73, height = 27;
|
||||||
|
int bottom = surface - drop;
|
||||||
|
if (bottom < 10 || bottom + height >= surface - 4) { p.reject("depth-envelope"); continue; }
|
||||||
|
var c = new MineralSanctuaryPlan23.Chamber(x - width / 2, bottom, z - depth / 2,
|
||||||
|
width, height, depth, facing, mix(seed ^ ((long) x << 32) ^ z ^ drop));
|
||||||
|
if (openApproach(c, p) && coarse(c, p) && certify(c, p)) return result(c, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { p.reject(exhausted.reason); }
|
||||||
|
return result(null, p);
|
||||||
|
}
|
||||||
|
private static MineralSanctuaryPlan23 result(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||||
|
var result = new MineralSanctuaryPlan23(c, p.samples, p.columns.size(), p.rejections);
|
||||||
|
System.getLogger(MineralSanctuaryPlanner23.class.getName()).log(System.Logger.Level.INFO,
|
||||||
|
"Mineral23 accepted=" + (c != null) + " columns=" + p.columns.size() + " samples=" + p.samples
|
||||||
|
+ " chamber=" + c + " rejected=" + p.rejections);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/** The fissure ends in existing traversable air (a cave or the exposed island side), not sealed rock. */
|
||||||
|
private static boolean openApproach(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||||
|
int x = c.facing() == 1 ? c.width() - 1 : c.facing() == 3 ? 0 : c.width() / 2;
|
||||||
|
int z = c.facing() == 2 ? c.depth() - 1 : c.facing() == 0 ? 0 : c.depth() / 2;
|
||||||
|
int axis = c.facing() % 2 == 0 ? z : x;
|
||||||
|
int bend = (int) Math.round(Math.sin(axis * .13 + (c.seed() & 7)) * 2);
|
||||||
|
if (c.facing() % 2 == 0) x += bend; else z += bend;
|
||||||
|
int dx = c.facing() == 1 ? 1 : c.facing() == 3 ? -1 : 0;
|
||||||
|
int dz = c.facing() == 2 ? 1 : c.facing() == 0 ? -1 : 0;
|
||||||
|
int feet = c.minY() + c.floor(x, z) + 1;
|
||||||
|
for (int forward = 1; forward <= 2; forward++) for (int side = -1; side <= 1; side++) {
|
||||||
|
int wx = c.minX() + x + dx * forward + dz * side;
|
||||||
|
int wz = c.minZ() + z + dz * forward - dx * side;
|
||||||
|
if (!p.clear(wx, feet, wz) || !p.clear(wx, feet + 1, wz)) return p.reject("approach-blocked");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/** Package-local admission entry point also exercises real exposed-flank fixtures. */
|
||||||
|
static boolean admits(MineralSanctuaryPlan23.Chamber c, Terrain terrain) {
|
||||||
|
var p = new Probe(terrain);
|
||||||
|
try { return openApproach(c, p) && coarse(c, p) && certify(c, p); }
|
||||||
|
catch (BudgetExceeded exhausted) { return false; }
|
||||||
|
}
|
||||||
|
private static boolean coarse(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||||
|
int rock = 0, tested = 0, covered = 0, columns = 0, coveredQuadrants = 0;
|
||||||
|
for (int z = 5; z < c.depth(); z += 12) for (int x = 5; x < c.width(); x += 12) {
|
||||||
|
if (!c.footprint(x, z)) continue;
|
||||||
|
int wx = c.minX() + x, wz = c.minZ() + z;
|
||||||
|
columns++;
|
||||||
|
if (p.surface(wx, wz) >= c.minY() + 8) {
|
||||||
|
covered++;
|
||||||
|
coveredQuadrants |= 1 << ((x < c.width() / 2 ? 0 : 1) + (z < c.depth() / 2 ? 0 : 2));
|
||||||
|
}
|
||||||
|
for (int y : new int[]{c.minY() - 2, c.minY() + 5, c.maxY()}) {
|
||||||
|
if (p.protectedAt(wx, y, wz)) return p.reject("coarse-protected");
|
||||||
|
if (p.solid(wx, y, wz)) rock++;
|
||||||
|
tested++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (columns == 0 || covered * 5 < columns * 3 || coveredQuadrants != 15)
|
||||||
|
return p.reject("coarse-cover-distribution");
|
||||||
|
return tested > 0 && rock * 5 >= tested * 3 || p.reject("coarse-rock");
|
||||||
|
}
|
||||||
|
/** Reject unsupported candidates using a small shared column lattice before reading the full floor. */
|
||||||
|
private static boolean coarseBearings(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||||
|
var anchors = new java.util.ArrayList<int[]>();
|
||||||
|
var gaps = new java.util.ArrayList<int[]>();
|
||||||
|
var footprint = new java.util.ArrayList<int[]>();
|
||||||
|
for (int z : lattice(c.depth())) for (int x : lattice(c.width())) {
|
||||||
|
if (!c.footprint(x, z) && !c.accessFootprint(x, z)) continue;
|
||||||
|
var point = new int[]{x, z}; footprint.add(point);
|
||||||
|
int wx = c.minX() + x, wz = c.minZ() + z;
|
||||||
|
if (p.solid(wx, c.minY() - 1, wz) && p.solid(wx, c.minY() - 2, wz)) anchors.add(point);
|
||||||
|
else gaps.add(point);
|
||||||
|
}
|
||||||
|
// Sampling is deliberately permissive about the proportion; dense certification still requires60%.
|
||||||
|
if (anchors.size() < gaps.size()) return p.reject("coarse-anchor-majority");
|
||||||
|
for (var gap : gaps) {
|
||||||
|
boolean supported = false;
|
||||||
|
for (var anchor : anchors) {
|
||||||
|
int dx = gap[0] - anchor[0], dz = gap[1] - anchor[1];
|
||||||
|
if (dx * dx + dz * dz <= 100) { supported = true; break; }
|
||||||
|
}
|
||||||
|
if (!supported) return p.reject("coarse-anchor-distance");
|
||||||
|
}
|
||||||
|
// A protected layer between the three broad rock witnesses must also reject before a dense scan.
|
||||||
|
for (var point : footprint) for (int y = -2; y < c.height() + 2; y += 4)
|
||||||
|
if (p.protectedAt(c.minX() + point[0], c.minY() + y, c.minZ() + point[1]))
|
||||||
|
return p.reject("coarse-protected-layer");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static java.util.List<Integer> lattice(int size) {
|
||||||
|
var result = new java.util.ArrayList<Integer>();
|
||||||
|
for (int i = 0; i < size; i += 5) result.add(i);
|
||||||
|
if (result.getLast() != size - 1) result.add(size - 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
private static boolean certify(MineralSanctuaryPlan23.Chamber c, Probe p) {
|
||||||
|
if (!coarseBearings(c, p)) return false;
|
||||||
|
// Every generated floor column is classified, including exposed edges and the fissure.
|
||||||
|
// A majority bears directly on two native rock blocks; each remaining point must be
|
||||||
|
// within ten horizontal blocks of such a bearing. No rescue pier extends the envelope.
|
||||||
|
boolean[][] footprint = new boolean[c.width()][c.depth()];
|
||||||
|
boolean[][] anchored = new boolean[c.width()][c.depth()];
|
||||||
|
int columns = 0, direct = 0, covered = 0;
|
||||||
|
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
|
||||||
|
if (!c.footprint(x, z) && !c.accessFootprint(x, z)) continue;
|
||||||
|
footprint[x][z] = true; columns++;
|
||||||
|
int wx = c.minX() + x, wz = c.minZ() + z;
|
||||||
|
if (!p.terrain.owns(wx, wz)) return p.reject("outside-owner");
|
||||||
|
if (p.surface(wx, wz) >= c.minY() + 6) covered++;
|
||||||
|
if (p.solid(wx, c.minY() - 1, wz) && p.solid(wx, c.minY() - 2, wz)) {
|
||||||
|
anchored[x][z] = true; direct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (columns == 0 || direct * 5 < columns * 3) return p.reject("floor-anchor-majority");
|
||||||
|
if (covered * 5 < columns * 3) return p.reject("full-cover-distribution");
|
||||||
|
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
|
||||||
|
if (!footprint[x][z] || anchored[x][z]) continue;
|
||||||
|
boolean nearby = false;
|
||||||
|
for (int dz = -10; dz <= 10 && !nearby; dz++) for (int dx = -10; dx <= 10; dx++) {
|
||||||
|
int ax = x + dx, az = z + dz;
|
||||||
|
if (dx * dx + dz * dz <= 100 && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
|
||||||
|
&& anchored[ax][az]) { nearby = true; break; }
|
||||||
|
}
|
||||||
|
if (!nearby) return p.reject("floor-anchor-distance");
|
||||||
|
}
|
||||||
|
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
|
||||||
|
if (!footprint[x][z]) continue;
|
||||||
|
for (int y = -2; y < c.height() + 2; y++)
|
||||||
|
if (p.protectedAt(c.minX() + x, c.minY() + y, c.minZ() + z)) return p.reject("protected-volume");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private static final class Probe {
|
||||||
|
final Terrain terrain; final Map<Long, Integer> columns = new HashMap<>();
|
||||||
|
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples;
|
||||||
|
Probe(Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
|
||||||
|
int surface(int x, int z) {
|
||||||
|
if (!terrain.owns(x, z)) return -1;
|
||||||
|
long k = (long) x << 32 ^ z & 0xffffffffL;
|
||||||
|
Integer known = columns.get(k); if (known != null) return known;
|
||||||
|
if (columns.size() >= MAX_COLUMNS) throw new BudgetExceeded("column-budget");
|
||||||
|
count(); int top = terrain.surfaceY(x, z); columns.put(k, top); return top;
|
||||||
|
}
|
||||||
|
boolean solid(int x, int y, int z) {
|
||||||
|
if (y < 1 || y > 382 || surface(x, z) < 0) return false;
|
||||||
|
count(); return terrain.solid(x, y, z);
|
||||||
|
}
|
||||||
|
boolean protectedAt(int x, int y, int z) {
|
||||||
|
if (y < 1 || y > 382 || !terrain.owns(x, z)) return true;
|
||||||
|
surface(x, z); // Owned native void can be used by an attached exposed edge.
|
||||||
|
count(); return terrain.protectedAt(x, y, z);
|
||||||
|
}
|
||||||
|
boolean clear(int x, int y, int z) {
|
||||||
|
if (y < 1 || y > 382 || !terrain.owns(x, z)) return false;
|
||||||
|
surface(x, z); // Charge/cache even an exposed native column with no dry surface.
|
||||||
|
count(); if (terrain.solid(x, y, z)) return false;
|
||||||
|
count(); return !terrain.protectedAt(x, y, z);
|
||||||
|
}
|
||||||
|
boolean reject(String reason) { rejections.merge(reason, 1, Integer::sum); return false; }
|
||||||
|
void count() { if (samples >= MAX_SAMPLES) throw new BudgetExceeded("sample-budget"); samples++; }
|
||||||
|
}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException { final String reason; BudgetExceeded(String reason) { this.reason = reason; } }
|
||||||
|
private static double unit(long n) { return (n >>> 11) * 0x1.0p-53; }
|
||||||
|
private static long mix(long n) { n = (n ^ n >>> 30) * 0xbf58476d1ce4e5b9L; n = (n ^ n >>> 27) * 0x94d049bb133111ebL; return n ^ n >>> 31; }
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/** Rock terraces, irregular vault and rooted columns. No furniture, masonry or manufactured reward. */
|
||||||
|
public final class MineralSanctuaryRenderer23 {
|
||||||
|
private MineralSanctuaryRenderer23() {}
|
||||||
|
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 && !c.centralFloor(x, z) && !c.pillar(x, z)
|
||||||
|
&& (state.is(Blocks.STONE) || state.is(Blocks.COBBLESTONE))) {
|
||||||
|
// The lower half-step faces the lower terrace. Solid rock below remains intact.
|
||||||
|
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) {
|
||||||
|
state = (state.is(Blocks.COBBLESTONE) ? Blocks.COBBLESTONE_STAIRS : Blocks.STONE_STAIRS)
|
||||||
|
.defaultBlockState().setValue(StairBlock.FACING, low.getOpposite()); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.set(x, y, z, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||||
|
|
||||||
|
public final class MineralSanctuaryStructure23 extends Structure {
|
||||||
|
public static final MapCodec<MineralSanctuaryStructure23> CODEC = simpleCodec(MineralSanctuaryStructure23::new);
|
||||||
|
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); }
|
||||||
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|
|| generator.rootGeneration() != 23 || !generator.experimentalStructures()) return Optional.empty();
|
||||||
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
|
if (island == null || !island.origin() || island.generation() != 23) 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))));
|
||||||
|
}
|
||||||
|
@Override public StructureType<?> type() { return MineralSanctuaryStructures23.TYPE; }
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||||
|
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
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.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
/** Native persistence and locate for the generation23 mineral vault. */
|
||||||
|
public final class MineralSanctuaryStructures23 {
|
||||||
|
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE,
|
||||||
|
SanctuaryMod.id("mineral_sanctuary"));
|
||||||
|
public static final StructureType<MineralSanctuaryStructure23> TYPE = Registry.register(
|
||||||
|
BuiltInRegistries.STRUCTURE_TYPE, SanctuaryMod.id("mineral_sanctuary_v23"), () -> MineralSanctuaryStructure23.CODEC);
|
||||||
|
|
||||||
|
public static final net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType PIECE = Registry.register(
|
||||||
|
BuiltInRegistries.STRUCTURE_PIECE, SanctuaryMod.id("mineral_sanctuary_piece_v23"), MineralSanctuaryPiece23::new);
|
||||||
|
private MineralSanctuaryStructures23() {}
|
||||||
|
public static void register() { /* Initialize the codec before reading the dynamic structure registry. */ }
|
||||||
|
|
||||||
|
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||||
|
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 || !sanctuary.experimentalStructures()) return;
|
||||||
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
|
if (island == null || !island.origin() || island.generation() != 23) return;
|
||||||
|
var plan = sanctuary.mineralSanctuary23();
|
||||||
|
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|
||||||
|
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return;
|
||||||
|
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
|
Structure structure = holder.value();
|
||||||
|
StructureStart previous = manager.getStartForStructure(structure, chunk);
|
||||||
|
if (previous != null && previous.isValid()) return;
|
||||||
|
var random = generator.randomState(island);
|
||||||
|
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||||
|
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||||
|
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||||
|
chunk, structure.biomes()::contains);
|
||||||
|
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Locate returns the persisted vault entrance, loading only the native structure start when necessary. */
|
||||||
|
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||||
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|
|| generator.rootGeneration() != 23 || !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.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);
|
||||||
|
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||||
|
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) return null;
|
||||||
|
var piece = start.getPieces().stream().filter(p -> p instanceof MineralSanctuaryPiece23).findFirst();
|
||||||
|
if (piece.isPresent()) {
|
||||||
|
if (skipReferenced) level.structureManager().addReference(start);
|
||||||
|
return Pair.of(piece.get().getLocatorPosition(), holder);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||||
|
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||||
|
static void placing(Runnable run) {
|
||||||
|
int depth = PLACEMENT_DEPTH.get(); PLACEMENT_DEPTH.set(depth + 1);
|
||||||
|
try { run.run(); } finally { if (depth == 0) PLACEMENT_DEPTH.remove(); else PLACEMENT_DEPTH.set(depth); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.ruins;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Network;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Support;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Generation23 selects one buried workshop with its real access and connecting corridor.
|
||||||
|
* The broader17 catalogue remains available only to the historical generators. */
|
||||||
|
public final class RuinsPlanner23 {
|
||||||
|
public static final int MAX_COLUMNS = 5_000;
|
||||||
|
public static final int MAX_SAMPLES = RuinsPlan17.MAX_SAMPLES;
|
||||||
|
private static final int INVALID = Integer.MIN_VALUE;
|
||||||
|
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||||
|
private RuinsPlanner23() {}
|
||||||
|
|
||||||
|
public static RuinsPlan17 plan(long seed, int centerX, int centerZ, int diameter, RuinsPlanner17.Terrain terrain) {
|
||||||
|
if (diameter < 64 || diameter > 1024) throw new IllegalArgumentException("Unsupported ruins island size");
|
||||||
|
var probe = new Probe(terrain, centerX, centerZ, diameter);
|
||||||
|
var networks = new ArrayList<Network>();
|
||||||
|
for (var family : List.of(Family.MINE)) {
|
||||||
|
int remaining = 0;
|
||||||
|
probe.columnLimit = MAX_COLUMNS - remaining * (MAX_COLUMNS / 3);
|
||||||
|
probe.sampleLimit = MAX_SAMPLES - remaining * (MAX_SAMPLES / 3);
|
||||||
|
Network best = null;
|
||||||
|
try {
|
||||||
|
search: for (int attempt = 0; attempt < 6; attempt++) {
|
||||||
|
Site site = site(seed, family, attempt, centerX, centerZ, diameter, probe);
|
||||||
|
if (site == null) continue;
|
||||||
|
for (int drop : new int[] {14, 22, 30}) {
|
||||||
|
Network candidate = network(seed, family, site, drop, networks, probe);
|
||||||
|
if (candidate != null && (best == null || candidate.rooms().size() > best.rooms().size())) best = candidate;
|
||||||
|
if (best != null && best.rooms().size() == 3) break search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BudgetExceeded exhausted) { probe.reject(family.name().toLowerCase(Locale.ROOT) + "-budget"); }
|
||||||
|
if (best != null) networks.add(best);
|
||||||
|
}
|
||||||
|
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.ruins.plannerDiagnostics"))
|
||||||
|
System.getLogger(RuinsPlanner23.class.getName()).log(System.Logger.Level.INFO,
|
||||||
|
"Ruins23 seed=" + seed + " diameter=" + diameter + " networks="
|
||||||
|
+ networks.stream().map(n -> n.family() + ":" + n.rooms().size()).toList()
|
||||||
|
+ " columns=" + probe.columns.size() + " samples=" + probe.samples + " rejected=" + probe.rejections);
|
||||||
|
return new RuinsPlan17(networks, probe.samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Site site(long seed, Family family, int attempt, int cx, int cz, int diameter, Probe probe) {
|
||||||
|
long salt = mix(seed ^ family.ordinal() * 0x9E3779B97F4A7C15L ^ attempt * 0xD1342543DE82EF95L ^ 0x1700L);
|
||||||
|
double base = unit(mix(seed ^ 0x17BA5EL)) * Math.PI * 2 + family.ordinal() * Math.PI * 2 / 3;
|
||||||
|
double angle = base + new double[] {0, .30, -.30, .48, -.48, .72}[attempt];
|
||||||
|
double radius = Math.min(diameter * .27, 216) * (attempt == 3 || attempt == 4 ? .72 : 1);
|
||||||
|
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||||
|
if (Math.hypot(x - cx, z - cz) < 52) return null;
|
||||||
|
int low = 384, high = -1;
|
||||||
|
// Five coarse witnesses reject cliffs, water and unowned corners before a volume scan.
|
||||||
|
for (int[] offset : new int[][] {{-2, -2}, {-2, 2}, {2, -2}, {2, 2}, {0, 0}}) {
|
||||||
|
int px = x + offset[0], pz = z + offset[1], top = probe.surface(px, pz);
|
||||||
|
if (top < 54 || top > 376 || !probe.solid(px, top, pz) || probe.protectedAt(px, top, pz)) {
|
||||||
|
probe.reject("coarse-entrance"); return null;
|
||||||
|
}
|
||||||
|
low = Math.min(low, top); high = Math.max(high, top);
|
||||||
|
}
|
||||||
|
if (high - low > 4) { probe.reject("entrance-cliff"); return null; }
|
||||||
|
int direction = Math.abs(x - cx) >= Math.abs(z - cz) ? x < cx ? 1 : 3 : z < cz ? 2 : 0;
|
||||||
|
if ((salt & 1) != 0) direction = (direction + (attempt % 2 == 0 ? 1 : 3)) & 3;
|
||||||
|
return new Site(x, z, high, direction, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Network network(long seed, Family family, Site site, int drop, List<Network> previous, Probe probe) {
|
||||||
|
int top = site.top();
|
||||||
|
for (int z = site.z() - 2; z <= site.z() + 2; z++) for (int x = site.x() - 2; x <= site.x() + 2; x++) {
|
||||||
|
int height = probe.surface(x, z);
|
||||||
|
if (height < site.top() - 4 || height > site.top() + 4) { probe.reject("entrance-surface"); return null; }
|
||||||
|
top = Math.max(top, height);
|
||||||
|
}
|
||||||
|
int floor = top - drop;
|
||||||
|
Room access = new Room(Kind.ACCESS, site.x() - 2, floor - 1, site.z() - 2,
|
||||||
|
5, drop + 4, 5, 0, mix(seed ^ site.seed() ^ drop), List.of());
|
||||||
|
var rooms = new ArrayList<Room>();
|
||||||
|
access = certify(access, rooms, previous, probe);
|
||||||
|
if (access == null) return null;
|
||||||
|
rooms.add(access);
|
||||||
|
if (!attach(rooms, 0, Kind.CORRIDOR, site.direction(), mix(site.seed() ^ drop ^ 0xC0L), previous, probe)) return null;
|
||||||
|
if (!attach(rooms, 1, Kind.WORKSHOP, site.direction(), mix(site.seed() ^ drop ^ 0xA1L), previous, probe)) return null;
|
||||||
|
return new Network("hidden_workshop", family, site.x() >> 4, site.z() >> 4, rooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean attach(List<Room> rooms, int parentIndex, Kind kind, int direction, long seed,
|
||||||
|
List<Network> previous, Probe probe) {
|
||||||
|
Room parent = rooms.get(parentIndex);
|
||||||
|
int width = kind == Kind.CORRIDOR ? 5 : kind == Kind.MINE_GALLERY ? 13 : 9;
|
||||||
|
int depth = kind == Kind.CORRIDOR ? 5 : 9, height = kind == Kind.CORRIDOR ? 6 : 7;
|
||||||
|
int x = parent.centerX() - width / 2, z = parent.centerZ() - depth / 2;
|
||||||
|
if (direction == 0) z = parent.minZ() - depth;
|
||||||
|
if (direction == 1) x = parent.maxX() + 1;
|
||||||
|
if (direction == 2) z = parent.maxZ() + 1;
|
||||||
|
if (direction == 3) x = parent.minX() - width;
|
||||||
|
Room room = new Room(kind, x, parent.minY(), z, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
|
||||||
|
room = certify(room, rooms, previous, probe);
|
||||||
|
if (room == null) return false;
|
||||||
|
rooms.set(parentIndex, parent.withOpening(direction)); rooms.add(room);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Room certify(Room draft, List<Room> siblings, List<Network> previous, Probe probe) {
|
||||||
|
for (Room room : siblings) if (conflicts(room, draft)) { probe.reject("sibling-overlap"); return null; }
|
||||||
|
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, draft)) {
|
||||||
|
probe.reject("network-overlap"); return null;
|
||||||
|
}
|
||||||
|
for (int z = draft.minZ(); z <= draft.maxZ(); z++) for (int x = draft.minX(); x <= draft.maxX(); x++) {
|
||||||
|
int top = probe.surface(x, z);
|
||||||
|
if (top == INVALID || top < draft.minY() + 5 || draft.kind() != Kind.ACCESS && top < draft.maxY() + 1) {
|
||||||
|
probe.reject("room-exposed"); return null;
|
||||||
|
}
|
||||||
|
for (int y = draft.minY(); y <= draft.maxY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||||
|
probe.reject("room-protected"); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var supports = new ArrayList<Support>();
|
||||||
|
for (int z : new int[] {draft.minZ() + 1, draft.centerZ(), draft.maxZ() - 1})
|
||||||
|
for (int x : new int[] {draft.minX() + 1, draft.centerX(), draft.maxX() - 1}) {
|
||||||
|
int bottom = draft.minY() - 1;
|
||||||
|
if (probe.solid(x, bottom, z) && probe.solid(x, bottom - 1, z)) {
|
||||||
|
if (probe.protectedAt(x, bottom, z) || probe.protectedAt(x, bottom - 1, z)) { probe.reject("footing-protected"); return null; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int rock = -1;
|
||||||
|
for (int y = bottom - 1; y >= Math.max(9, draft.minY() - 13); y--)
|
||||||
|
if (probe.solid(x, y, z) && probe.solid(x, y - 1, z)) { rock = y; break; }
|
||||||
|
if (rock < 0) { probe.reject("footing-void"); return null; }
|
||||||
|
for (int y = rock; y <= bottom; y++) if (probe.protectedAt(x, y, z)) { probe.reject("pier-protected"); return null; }
|
||||||
|
supports.add(new Support(x, rock + 1, z, bottom));
|
||||||
|
}
|
||||||
|
Room result = draft.withSupports(supports);
|
||||||
|
for (Room sibling : siblings) if (conflicts(sibling, result)) { probe.reject("pier-sibling"); return null; }
|
||||||
|
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, result)) {
|
||||||
|
probe.reject("pier-network"); return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean conflicts(Room a, Room b) {
|
||||||
|
if (protectsBounds(a, b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())
|
||||||
|
|| protectsBounds(b, a.minX(), a.minY(), a.minZ(), a.maxX(), a.maxY(), a.maxZ())) return true;
|
||||||
|
for (var support : b.supports()) if (protectsBounds(a, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
|
||||||
|
for (var support : a.supports()) if (protectsBounds(b, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private static boolean protectsBounds(Room room, int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||||
|
if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
|
||||||
|
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
|
||||||
|
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
|
||||||
|
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
|
||||||
|
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
private record Site(int x, int z, int top, int direction, long seed) {}
|
||||||
|
private static final class BudgetExceeded extends RuntimeException {}
|
||||||
|
private static final class Probe {
|
||||||
|
final RuinsPlanner17.Terrain terrain; final int centerX, centerZ; final double radius;
|
||||||
|
final Map<Long, Integer> columns = new HashMap<>();
|
||||||
|
final Map<String, Integer> rejections = new java.util.TreeMap<>();
|
||||||
|
int samples, columnLimit, sampleLimit;
|
||||||
|
Probe(RuinsPlanner17.Terrain terrain, int centerX, int centerZ, int diameter) {
|
||||||
|
this.terrain = java.util.Objects.requireNonNull(terrain); this.centerX = centerX; this.centerZ = centerZ;
|
||||||
|
radius = diameter / 2.0 - 3;
|
||||||
|
}
|
||||||
|
int surface(int x, int z) {
|
||||||
|
if (Math.hypot((double) x - centerX, (double) z - centerZ) > radius || !terrain.owns(x, z)) return INVALID;
|
||||||
|
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||||
|
var cached = columns.get(key); if (cached != null) return cached;
|
||||||
|
if (columns.size() >= columnLimit) throw new BudgetExceeded();
|
||||||
|
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||||
|
}
|
||||||
|
boolean solid(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return false;
|
||||||
|
count(); return terrain.solid(x, y, z);
|
||||||
|
}
|
||||||
|
boolean protectedAt(int x, int y, int z) {
|
||||||
|
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return true;
|
||||||
|
count(); return terrain.protectedAt(x, y, z);
|
||||||
|
}
|
||||||
|
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(); samples++; }
|
||||||
|
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
|
||||||
|
}
|
||||||
|
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
|
|||||||
|
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()) return Optional.empty();
|
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||||
var networks = generator.ruinsPlan17().networks().stream()
|
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 -> {
|
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
// 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)
|
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) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
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) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
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 instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||||
|| !sanctuary.experimentalStructures()) return;
|
|| !sanctuary.experimentalStructures()) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||||
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || !generator.experimentalStructures()
|
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.experimentalStructures()
|
||||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
|
|||||||
public TransitStructure21(StructureSettings settings) { super(settings); }
|
public TransitStructure21(StructureSettings settings) { super(settings); }
|
||||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()) return Optional.empty();
|
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) return Optional.empty();
|
||||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return Optional.empty();
|
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && island.generation() != 23))) return Optional.empty();
|
||||||
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
|
var modules = generator.transitPlan21().modulesForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||||
if (modules.isEmpty()) return Optional.empty();
|
if (modules.isEmpty()) return Optional.empty();
|
||||||
for (var module : modules) {
|
for (var module : modules) {
|
||||||
@@ -25,7 +25,7 @@ public final class TransitStructure21 extends Structure {
|
|||||||
throw new IllegalStateException("Transit module exceeds local native reference range");
|
throw new IllegalStateException("Transit module exceeds local native reference range");
|
||||||
}
|
}
|
||||||
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
||||||
modules.forEach(module -> pieces.addPiece(generator.rootGeneration() == 22
|
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
|
||||||
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
|
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
|
||||||
? new TransitPiece22(module) : new TransitPiece21(module)))));
|
? new TransitPiece22(module) : new TransitPiece21(module)))));
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -46,10 +46,10 @@ public final class TransitStructures21 {
|
|||||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && generator.rootGeneration() != 22)
|
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||||
|| generator.rootGeneration() != 22 && !sanctuary.experimentalStructures()) return;
|
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !sanctuary.experimentalStructures()) return;
|
||||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||||
if (island == null || !island.origin() || (island.generation() != 21 && island.generation() != 22)) return;
|
if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && island.generation() != 23))) return;
|
||||||
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
|
if (sanctuary.transitPlan21().modulesForChunk(chunk.getPos().x(), chunk.getPos().z()).isEmpty()) return;
|
||||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
||||||
@@ -66,7 +66,7 @@ public final class TransitStructures21 {
|
|||||||
BlockPos origin, int radius, boolean skipReferenced) {
|
BlockPos origin, int radius, boolean skipReferenced) {
|
||||||
if (!Level.OVERWORLD.equals(level.dimension())
|
if (!Level.OVERWORLD.equals(level.dimension())
|
||||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||||
|| (generator.rootGeneration() != 21 && generator.rootGeneration() != 22) || generator.rootGeneration() != 22 && !generator.experimentalStructures()
|
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()
|
||||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||||
if (!structures.contains(holder)) return null;
|
if (!structures.contains(holder)) return null;
|
||||||
|
|||||||
@@ -206,5 +206,24 @@
|
|||||||
"sanctuary.history.action.picked_up": "Picked up",
|
"sanctuary.history.action.picked_up": "Picked up",
|
||||||
"sanctuary.history.action.dropped": "Dropped",
|
"sanctuary.history.action.dropped": "Dropped",
|
||||||
"sanctuary.history.action.crafted": "Crafted",
|
"sanctuary.history.action.crafted": "Crafted",
|
||||||
"sanctuary.history.scope": "Observed actions since installation, grouped by UTC day across the server. These records do not measure consumption or ownership transfers."
|
"sanctuary.history.scope": "Observed actions since installation, grouped by UTC day across the server. These records do not measure consumption or ownership transfers.",
|
||||||
|
"sanctuary.world_options.secrets23_hint": "Natural island, mineral secrets and abandoned railways. Vanilla structures remain possible. Expansions may contain oceans.",
|
||||||
|
"sanctuary.expansion.relief.ocean": "Oceanic continent",
|
||||||
|
"sanctuary.expansion.error.native_structure_unavailable": "No suitable structure found: expansion was not reserved.",
|
||||||
|
"structure.sanctuary.mineral_sanctuary": "Mineral sanctuary",
|
||||||
|
"sanctuary.structures.census.unavailable": "Structure registry unavailable: coverage is unknown.",
|
||||||
|
"sanctuary.structures.census.summary": "Structures: %s starts, %s monster rooms, %s observed chunks.",
|
||||||
|
"sanctuary.structures.census.region": "%s / %s: %s; starts %s/%s, decoration %s/%s, full %s/%s.",
|
||||||
|
"sanctuary.structures.census.family": "%s: %s accepted, %s generated — %s",
|
||||||
|
"sanctuary.structures.census.status.present": "Present",
|
||||||
|
"sanctuary.structures.census.status.absent": "Confirmed absent",
|
||||||
|
"sanctuary.structures.census.status.unknown": "Unknown",
|
||||||
|
"sanctuary.structures.census.unknown_island": "Unknown island: %s",
|
||||||
|
"sanctuary.structures.census.limited": "Showing %s of %s results.",
|
||||||
|
"sanctuary.structures.census.help": "/sanctuary census structures [island <id> | dimension <id>] — historical registry and verified coverage.",
|
||||||
|
"sanctuary.structures.census.dimension": "Dimension %s: %s observed chunks.",
|
||||||
|
"sanctuary.structures.census.unknown_dimension": "Unknown dimension: %s",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,5 +206,24 @@
|
|||||||
"sanctuary.history.action.picked_up": "Ramassé",
|
"sanctuary.history.action.picked_up": "Ramassé",
|
||||||
"sanctuary.history.action.dropped": "Jeté",
|
"sanctuary.history.action.dropped": "Jeté",
|
||||||
"sanctuary.history.action.crafted": "Fabriqué",
|
"sanctuary.history.action.crafted": "Fabriqué",
|
||||||
"sanctuary.history.scope": "Actions observées depuis l’installation, regroupées par jour UTC pour tout le serveur. Ces relevés ne mesurent pas une consommation ni un transfert de propriété."
|
"sanctuary.history.scope": "Actions observées depuis l’installation, regroupées par jour UTC pour tout le serveur. Ces relevés ne mesurent pas une consommation ni un transfert de propriété.",
|
||||||
|
"sanctuary.world_options.secrets23_hint": "Île naturelle, secrets minéraux et voies abandonnées. Les structures vanilla restent possibles. Les expansions peuvent accueillir des océans.",
|
||||||
|
"sanctuary.expansion.relief.ocean": "Continent océanique",
|
||||||
|
"sanctuary.expansion.error.native_structure_unavailable": "Aucune structure adaptée trouvée : expansion non réservée.",
|
||||||
|
"structure.sanctuary.mineral_sanctuary": "Sanctuaire minéral",
|
||||||
|
"sanctuary.structures.census.unavailable": "Registre des structures indisponible : couverture inconnue.",
|
||||||
|
"sanctuary.structures.census.summary": "Structures : %s départs, %s donjons, %s chunks observés.",
|
||||||
|
"sanctuary.structures.census.region": "%s / %s : %s ; départs %s/%s, décoration %s/%s, complets %s/%s.",
|
||||||
|
"sanctuary.structures.census.family": "%s : %s implantations, %s générées — %s",
|
||||||
|
"sanctuary.structures.census.status.present": "Présente",
|
||||||
|
"sanctuary.structures.census.status.absent": "Absence confirmée",
|
||||||
|
"sanctuary.structures.census.status.unknown": "Inconnue",
|
||||||
|
"sanctuary.structures.census.unknown_island": "Île inconnue : %s",
|
||||||
|
"sanctuary.structures.census.limited": "Affichage limité à %s résultats sur %s.",
|
||||||
|
"sanctuary.structures.census.help": "/sanctuary census structures [island <id> | dimension <id>] — registre historique et couverture vérifiée.",
|
||||||
|
"sanctuary.structures.census.dimension": "Dimension %s : %s chunks observés.",
|
||||||
|
"sanctuary.structures.census.unknown_dimension": "Dimension inconnue : %s",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"type": "sanctuary:mineral_sanctuary_v23",
|
||||||
|
"biomes": "#sanctuary:has_structure/forgotten_networks",
|
||||||
|
"step": "underground_structures",
|
||||||
|
"terrain_adaptation": "none",
|
||||||
|
"spawn_overrides": {}
|
||||||
|
}
|
||||||
+16
-3
@@ -3,7 +3,7 @@
|
|||||||
"minecraft:overworld": {
|
"minecraft:overworld": {
|
||||||
"type": "sanctuary:sanctuary_384",
|
"type": "sanctuary:sanctuary_384",
|
||||||
"generator": {
|
"generator": {
|
||||||
"type": "sanctuary:island_v22",
|
"type": "sanctuary:island_v23",
|
||||||
"biome_source": {
|
"biome_source": {
|
||||||
"biomes": [
|
"biomes": [
|
||||||
"minecraft:the_void",
|
"minecraft:the_void",
|
||||||
@@ -50,8 +50,21 @@
|
|||||||
"sanctuary:population_sulfur_depths",
|
"sanctuary:population_sulfur_depths",
|
||||||
"sanctuary:population_lush_caves",
|
"sanctuary:population_lush_caves",
|
||||||
"sanctuary:population_dripstone_caves",
|
"sanctuary:population_dripstone_caves",
|
||||||
"sanctuary:lost_city"
|
"sanctuary:lost_city",
|
||||||
]
|
"minecraft:ocean",
|
||||||
|
"minecraft:deep_ocean",
|
||||||
|
"minecraft:cold_ocean",
|
||||||
|
"minecraft:deep_cold_ocean",
|
||||||
|
"minecraft:frozen_ocean",
|
||||||
|
"minecraft:deep_frozen_ocean",
|
||||||
|
"minecraft:warm_ocean",
|
||||||
|
"minecraft:lukewarm_ocean",
|
||||||
|
"minecraft:deep_lukewarm_ocean",
|
||||||
|
"minecraft:beach",
|
||||||
|
"minecraft:snowy_beach",
|
||||||
|
"minecraft:stony_shore"
|
||||||
|
],
|
||||||
|
"type": "sanctuary:island_v23"
|
||||||
},
|
},
|
||||||
"initial_diameter": 724
|
"initial_diameter": 724
|
||||||
}
|
}
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"dimensions": {
|
||||||
|
"minecraft:overworld": {
|
||||||
|
"type": "sanctuary:sanctuary_384",
|
||||||
|
"generator": {
|
||||||
|
"type": "sanctuary:island_v22",
|
||||||
|
"biome_source": {
|
||||||
|
"biomes": [
|
||||||
|
"minecraft:the_void",
|
||||||
|
"minecraft:forest",
|
||||||
|
"minecraft:birch_forest",
|
||||||
|
"minecraft:flower_forest",
|
||||||
|
"minecraft:plains",
|
||||||
|
"minecraft:meadow",
|
||||||
|
"minecraft:taiga",
|
||||||
|
"minecraft:snowy_taiga",
|
||||||
|
"minecraft:snowy_plains",
|
||||||
|
"minecraft:grove",
|
||||||
|
"minecraft:ice_spikes",
|
||||||
|
"minecraft:frozen_peaks",
|
||||||
|
"minecraft:old_growth_spruce_taiga",
|
||||||
|
"minecraft:dark_forest",
|
||||||
|
"minecraft:swamp",
|
||||||
|
"minecraft:pale_garden",
|
||||||
|
"minecraft:jungle",
|
||||||
|
"minecraft:sparse_jungle",
|
||||||
|
"minecraft:bamboo_jungle",
|
||||||
|
"minecraft:mangrove_swamp",
|
||||||
|
"minecraft:savanna",
|
||||||
|
"minecraft:savanna_plateau",
|
||||||
|
"minecraft:desert",
|
||||||
|
"minecraft:badlands",
|
||||||
|
"minecraft:eroded_badlands",
|
||||||
|
"minecraft:wooded_badlands",
|
||||||
|
"minecraft:windswept_hills",
|
||||||
|
"minecraft:windswept_gravelly_hills",
|
||||||
|
"minecraft:stony_peaks",
|
||||||
|
"minecraft:dappled_forest",
|
||||||
|
"minecraft:cherry_grove",
|
||||||
|
"minecraft:lush_caves",
|
||||||
|
"minecraft:dripstone_caves",
|
||||||
|
"minecraft:sulfur_caves",
|
||||||
|
"sanctuary:population_oak_forest",
|
||||||
|
"sanctuary:population_birch_forest",
|
||||||
|
"sanctuary:population_clearing",
|
||||||
|
"sanctuary:population_dry_woodland",
|
||||||
|
"sanctuary:population_rocky_heath",
|
||||||
|
"sanctuary:population_dark_grove",
|
||||||
|
"sanctuary:population_bamboo_grove",
|
||||||
|
"sanctuary:population_sulfur_depths",
|
||||||
|
"sanctuary:population_lush_caves",
|
||||||
|
"sanctuary:population_dripstone_caves",
|
||||||
|
"sanctuary:lost_city"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"initial_diameter": 724
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minecraft:the_end": {
|
||||||
|
"type": "minecraft:the_end",
|
||||||
|
"generator": {
|
||||||
|
"type": "minecraft:noise",
|
||||||
|
"biome_source": {
|
||||||
|
"type": "minecraft:the_end"
|
||||||
|
},
|
||||||
|
"settings": "minecraft:end"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minecraft:the_nether": {
|
||||||
|
"type": "minecraft:the_nether",
|
||||||
|
"generator": {
|
||||||
|
"type": "minecraft:noise",
|
||||||
|
"biome_source": {
|
||||||
|
"type": "minecraft:multi_noise",
|
||||||
|
"preset": "minecraft:nether"
|
||||||
|
},
|
||||||
|
"settings": "minecraft:nether"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,16 @@
|
|||||||
"ExpansionVoidTicksAccess",
|
"ExpansionVoidTicksAccess",
|
||||||
"ExpansionReuseChunkMarkerMixin",
|
"ExpansionReuseChunkMarkerMixin",
|
||||||
"ExpansionReusePromotionMixin",
|
"ExpansionReusePromotionMixin",
|
||||||
"ExpansionReuseSerializationMixin"
|
"ExpansionReuseSerializationMixin",
|
||||||
|
"MonumentChildren23Accessor",
|
||||||
|
"NativeStructureStart23Mixin",
|
||||||
|
"NativeStructureWater23Mixin",
|
||||||
|
"StructureCensusStartsMixin",
|
||||||
|
"StructureCensusFeaturesMixin",
|
||||||
|
"StructureCensusMonsterRoomMixin",
|
||||||
|
"RuinedPortal23Accessor",
|
||||||
|
"IglooHeight23Mixin",
|
||||||
|
"ScatteredHeight23Accessor"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
"client.SanctuarySkyMixin",
|
"client.SanctuarySkyMixin",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Geography assertions independent of chunk loading, world mutation and native decoration. */
|
||||||
|
public final class ExpansionOcean23Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int profiles = 0;
|
||||||
|
for (long seed : new long[] {0, 42, 2026}) for (int diameter : new int[] {512, 1024}) {
|
||||||
|
var island = new ExpansionIsland("ocean_test", "sanctuary", "east", "temperate", diameter,
|
||||||
|
2048, -1024, seed, "ocean", "reserved", 23);
|
||||||
|
long water = 0; int deepest = 0;
|
||||||
|
for (int x = -diameter / 2; x <= diameter / 2; x += 2)
|
||||||
|
for (int z = -diameter / 2; z <= diameter / 2; z += 2) {
|
||||||
|
var c = ExpansionOcean23.column(island, 2048 + x, -1024 + z);
|
||||||
|
if (c.water()) {
|
||||||
|
water++; deepest = Math.max(deepest, c.depth());
|
||||||
|
check(c.floorY() - c.bottomY() >= 14, "Retaining shell thickness");
|
||||||
|
check(ExpansionOcean23.density(c, c.floorY() - 2, -1) > 0, "Rock beneath the basin");
|
||||||
|
check(ExpansionOcean23.density(c, c.waterLevel() + 1, 1) < 0, "Open sky above the basin");
|
||||||
|
check(c.depth() <= 56 && c.depth() >= 1, "Bounded depth");
|
||||||
|
}
|
||||||
|
check(c.equals(ExpansionOcean23.column(island, 2048 + x, -1024 + z)), "Order-independent field");
|
||||||
|
}
|
||||||
|
double fraction = water * 4.0 / (Math.PI * diameter * diameter / 4.0);
|
||||||
|
check(fraction >= .30 && fraction <= .45, "Mixed land/ocean area " + fraction);
|
||||||
|
check(deepest >= 40, "A real deep ocean");
|
||||||
|
var outside = ExpansionOcean23.column(island, 2048 + diameter, -1024);
|
||||||
|
check(!outside.water() && ExpansionOcean23.density(outside, 100, -1) == -1, "Exterior void stays empty");
|
||||||
|
for (String climate : List.of("temperate", "boreal", "tropical", "glacial")) {
|
||||||
|
var variant = new ExpansionIsland("ocean_test", "sanctuary", "east", climate, diameter,
|
||||||
|
2048, -1024, seed, "ocean", "reserved", 23);
|
||||||
|
var c = ExpansionOcean23.column(variant, 2048, -1024);
|
||||||
|
check(ExpansionOcean23.BIOMES.contains(ExpansionOcean23.biome(variant, c)), "Registered native biome");
|
||||||
|
check(c.deep(), "Central depth supports ocean structures");
|
||||||
|
}
|
||||||
|
profiles++;
|
||||||
|
}
|
||||||
|
for (int version : new int[] {12, 14, 21, 22, 23}) {
|
||||||
|
check(!ExpansionOcean23.active(ExpansionIsland.origin(512, 42, version)), "Starter geography unchanged");
|
||||||
|
if (version != 23) check(ExpansionOcean23.quickRelief(42, "island_1", 1024, "humid", version).equals("natural"), "Historical quick unchanged");
|
||||||
|
}
|
||||||
|
for (int small : new int[] {64, 128, 256})
|
||||||
|
check(ExpansionOcean23.quickRelief(42, "island_1", small, "humid", 23).equals("natural"), "No tiny ocean");
|
||||||
|
int automatic = 0;
|
||||||
|
for (int i = 0; i < 300; i++) if (ExpansionOcean23.quickRelief(42, "island_" + i, 512, "humid", 23).equals("ocean")) automatic++;
|
||||||
|
check(automatic > 60 && automatic < 140, "Deterministic one-third choice");
|
||||||
|
System.out.println("Ocean23: " + profiles + " geographic profiles, native temperatures and historical isolation passed");
|
||||||
|
}
|
||||||
|
private static void check(boolean condition, String message) {
|
||||||
|
if (!condition) throw new AssertionError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package fr.koka.sanctuary.expansion;
|
||||||
|
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import java.io.IOException;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
/** Priorities are frozen descriptor data, including the immutable void-certificate identity. */
|
||||||
|
public final class ExpansionPriorities23Smoke {
|
||||||
|
private static final long SEED = Long.MAX_VALUE - 23;
|
||||||
|
private ExpansionPriorities23Smoke() {}
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
for (int generation : new int[]{12, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23}) {
|
||||||
|
var origin = ExpansionIsland.origin(512, SEED, generation);
|
||||||
|
var encoded = ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, origin).getOrThrow();
|
||||||
|
require(!encoded.getAsJsonObject().has("structure_priorities"), "Neutral and historical codecs omit priorities");
|
||||||
|
require(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow().equals(origin), "Legacy neutral codec round trip");
|
||||||
|
reject(() -> origin.withStructurePriorities(List.of("village")), "Priority profile on initial island");
|
||||||
|
}
|
||||||
|
var source = new ArrayList<>(List.of("village", "mansion"));
|
||||||
|
var child = island(23, 512, "natural").withStructurePriorities(source); source.add("camp");
|
||||||
|
require(child.structurePriorities().equals(List.of("mansion", "village")), "Sorted immutable profile");
|
||||||
|
reject(() -> child.structurePriorities().add("camp"), "Mutable profile");
|
||||||
|
reject(() -> child.withStructurePriorities(List.of("camp", "camp")), "Duplicate priority family");
|
||||||
|
reject(() -> island(22, 512, "natural").withStructurePriorities(List.of("camp")), "Priorities on historical generation");
|
||||||
|
require(child.ready().structurePriorities().equals(child.structurePriorities()), "Completion retains priorities");
|
||||||
|
require(child.withReuseToken("1".repeat(64)).structurePriorities().equals(child.structurePriorities()), "Certificate binding retains priorities");
|
||||||
|
var certificateIdentity = child.withReuseToken("").ready();
|
||||||
|
var encoded = ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, certificateIdentity).getOrThrow();
|
||||||
|
require(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow().equals(certificateIdentity), "Certificate codec preserves the complete profile");
|
||||||
|
var changed = child.withStructurePriorities(List.of("camp")).ready();
|
||||||
|
require(!changed.equals(certificateIdentity) && !ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, changed).getOrThrow().equals(encoded),
|
||||||
|
"Changing a priority changes the certificate identity");
|
||||||
|
require(island(23, 512, "ocean").relief().equals("ocean") && island(23, 1024, "ocean").relief().equals("ocean"), "Large generation23 ocean profiles");
|
||||||
|
reject(() -> island(23, 64, "ocean"), "Ocean on a small island");
|
||||||
|
reject(() -> island(22, 512, "ocean"), "Ocean on a historical generation");
|
||||||
|
require(!ExpansionIsland.reliefs(22).contains("ocean") && ExpansionIsland.reliefs(23).contains("ocean"), "Legacy relief catalogue remains stable");
|
||||||
|
|
||||||
|
Path scratch = Files.createTempDirectory("sanctuary-priorities23-");
|
||||||
|
try {
|
||||||
|
var directory = scratch.resolve("current"); var journal = new ExpansionJournal(directory, SEED, 512, 23);
|
||||||
|
var root = journal.islands().getFirst(); journal.commit(List.of(root, child));
|
||||||
|
var file = directory.resolve(ExpansionJournal.FILE_NAME); byte[] reserved = Files.readAllBytes(file);
|
||||||
|
require(new ExpansionJournal(directory, SEED, 512, 23).islands().get(1).equals(child), "Reserved priority profile survives restart");
|
||||||
|
reject(() -> journal.commit(List.of(root, child.withStructurePriorities(List.of("camp")))), "Changing priorities on an existing reservation");
|
||||||
|
require(Arrays.equals(reserved, Files.readAllBytes(file)), "Refused profile mutation leaves journal bytes unchanged");
|
||||||
|
journal.commit(List.of(root, child.ready()));
|
||||||
|
require(new ExpansionJournal(directory, SEED, 512, 23).islands().get(1).equals(child.ready()), "Ready journal replays identical priorities");
|
||||||
|
var legacyDirectory = scratch.resolve("legacy"); var legacy = new ExpansionJournal(legacyDirectory, SEED, 512, 22);
|
||||||
|
legacy.commit(List.of(legacy.islands().getFirst(), island(22, 512, "natural")));
|
||||||
|
var oldFile = legacyDirectory.resolve(ExpansionJournal.FILE_NAME); String historical = Files.readString(oldFile);
|
||||||
|
require(!historical.contains("structure_priorities"), "Manual journal encoder leaves historical format unchanged");
|
||||||
|
Files.writeString(oldFile, historical.replace("\"generation\": 22", "\"generation\": 22, \"structure_priorities\": []"));
|
||||||
|
reject(() -> new ExpansionJournal(legacyDirectory, SEED, 512, 22), "New optional field on a historical journal");
|
||||||
|
} finally {
|
||||||
|
try (var paths = Files.walk(scratch)) { for (var path : paths.sorted(Comparator.reverseOrder()).toList()) Files.deleteIfExists(path); }
|
||||||
|
}
|
||||||
|
System.out.println("Generation23 priorities: stable legacy formats, immutable capture, certificate identity, reservation reload and bounded ocean profiles.");
|
||||||
|
}
|
||||||
|
private static ExpansionIsland island(int generation, int diameter, String relief) {
|
||||||
|
return new ExpansionIsland("north_one", "sanctuary", "north", "temperate", diameter, 0, -2048,
|
||||||
|
ExpansionIsland.deriveSeed(SEED, "north_one"), relief, "reserved", generation);
|
||||||
|
}
|
||||||
|
@FunctionalInterface private interface Attempt { void run() throws Exception; }
|
||||||
|
private static void reject(Attempt attempt, String message) throws Exception {
|
||||||
|
try { attempt.run(); } catch (IOException | IllegalArgumentException | IllegalStateException | UnsupportedOperationException expected) { return; }
|
||||||
|
throw new AssertionError("Accepted " + message);
|
||||||
|
}
|
||||||
|
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package fr.koka.sanctuary.knowledge.structures;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Native-start deduplication and bounded coverage are independent of loaded-world snapshots. */
|
||||||
|
public final class StructureCensusSmoke {
|
||||||
|
private static final String OVERWORLD = "minecraft:overworld", NETHER = "minecraft:the_nether";
|
||||||
|
private static final long SEED = Long.MIN_VALUE + 23;
|
||||||
|
private StructureCensusSmoke() {}
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
var memory = new StructureCensusMemory();
|
||||||
|
var first = new ChunkKey(OVERWORLD, -1, 0); var next = new ChunkKey(OVERWORLD, 0, 0);
|
||||||
|
var expected = List.of(first, next);
|
||||||
|
require(!memory.snapshot().coverage(expected).startsComplete(), "Unobserved terrain is unknown");
|
||||||
|
var village = new Site(new SiteKey(OVERWORLD, "minecraft:village_plains", -1, 0, 0, false),
|
||||||
|
List.of(new Bounds(-8, 50, 0, 8, 60, 8)));
|
||||||
|
require(memory.observe(first, StructureCensusMemory.STARTS, List.of(village)), "New native start is recorded");
|
||||||
|
require(!memory.observe(first, StructureCensusMemory.STARTS, List.of(village)), "A repeated native start is not counted twice");
|
||||||
|
require(memory.snapshot().sites().size() == 1, "References in other chunks must not duplicate the start");
|
||||||
|
require(!memory.snapshot().generated(village), "An accepted start is not a fully generated structure");
|
||||||
|
memory.observe(first, StructureCensusMemory.STARTS | StructureCensusMemory.FULL, List.of(village));
|
||||||
|
require(!memory.snapshot().generated(village), "FULL departure does not prove its neighbouring pieces generated");
|
||||||
|
memory.observe(next, StructureCensusMemory.STARTS | StructureCensusMemory.FULL, List.of());
|
||||||
|
var full = memory.snapshot();
|
||||||
|
require(full.generated(village), "All native piece chunks are now generated");
|
||||||
|
require(full.coverage(expected).startsComplete(), "Complete finite start scope admits an absence conclusion");
|
||||||
|
require(!full.coverage(expected).featuresComplete(), "Loading old FULL chunks cannot prove monster-room absence");
|
||||||
|
memory.observe(first, StructureCensusMemory.STARTS | StructureCensusMemory.FEATURES, List.of(village));
|
||||||
|
memory.observe(next, StructureCensusMemory.STARTS | StructureCensusMemory.FEATURES, List.of());
|
||||||
|
require(memory.snapshot().coverage(expected).featuresComplete(), "Successful FEATURES observations cover feature origins");
|
||||||
|
require(!full.coverage(expected).featuresComplete(), "Snapshots are immutable");
|
||||||
|
|
||||||
|
var room = room(OVERWORLD, -2, 30, 3); var deeper = room(OVERWORLD, -2, 18, 3);
|
||||||
|
require(memory.observeFeature(room) && !memory.observeFeature(room), "Placed feature deduplicates by exact origin");
|
||||||
|
memory.observeFeature(deeper);
|
||||||
|
require(memory.snapshot().sites().size() == 3, "Two monster rooms in one chunk are distinct");
|
||||||
|
memory.observeFeature(room(NETHER, -2, 30, 3));
|
||||||
|
require(memory.snapshot().sites().size() == 4, "Dimensions isolate coincident positions");
|
||||||
|
require(memory.snapshot().inChunks(Set.of(first)).size() == 3, "Chunk filtering includes only owned departures or feature origins");
|
||||||
|
reject(() -> memory.observe(next, StructureCensusMemory.STARTS, List.of(village)), "Foreign chunk start");
|
||||||
|
reject(() -> memory.observe(next, 8, List.of()), "Unknown coverage flag");
|
||||||
|
reject(() -> full.chunks().put(first, 1), "Mutable snapshot");
|
||||||
|
reject(() -> new Site(village.key(), List.of(new Bounds(0, 0, 0, 4096, 100, 4096))), "Excessive native coverage footprint");
|
||||||
|
var restored = new StructureCensusMemory(memory.snapshot());
|
||||||
|
restored.observe(first, StructureCensusMemory.STARTS, List.of());
|
||||||
|
require(restored.snapshot().sites().containsKey(village.key()) && restored.snapshot().sites().containsKey(room.key()),
|
||||||
|
"Restored historical evidence survives a later empty native snapshot");
|
||||||
|
memory.observe(first, StructureCensusMemory.STARTS, List.of());
|
||||||
|
require(memory.snapshot().sites().containsKey(village.key()), "Later missing metadata does not erase a historical native occurrence");
|
||||||
|
require(memory.snapshot().sites().containsKey(room.key()), "Lack of native metadata does not erase recorded feature success");
|
||||||
|
priorities(new StructureCensusMemory());
|
||||||
|
|
||||||
|
Path scratch = Files.createTempDirectory("sanctuary-structure-census-");
|
||||||
|
try {
|
||||||
|
var directory = scratch.resolve("normal"); var store = new StructureCensusStore(directory, SEED);
|
||||||
|
require(store.load().sites().isEmpty() && !Files.exists(directory), "Empty load performs no writes");
|
||||||
|
var snapshot = memory.snapshot(); store.save(snapshot); byte[] original = Files.readAllBytes(store.path());
|
||||||
|
require(new StructureCensusStore(directory, SEED).load().equals(snapshot), "Evidence and coverage survive restart");
|
||||||
|
store.save(snapshot); require(Arrays.equals(original, Files.readAllBytes(store.path())), "Repeated save is stable");
|
||||||
|
reject(() -> new StructureCensusStore(directory, SEED + 1).load(), "Foreign world identity");
|
||||||
|
reject(() -> new StructureCensusStore(scratch.resolve("unloaded"), SEED).save(snapshot), "Save without loading");
|
||||||
|
String json = Files.readString(store.path());
|
||||||
|
var bad = List.of(
|
||||||
|
json.replace("\"schema\":1", "\"schema\":2"),
|
||||||
|
json.replace("\"schema\":1", "\"schema\":1,\"schema\":1"),
|
||||||
|
json.replace("\"flags\":7", "\"flags\":8"),
|
||||||
|
json.replace("\"feature\":true", "\"feature\":\"true\""),
|
||||||
|
json.replace("\"schema\":1", "\"schema\":1,\"unknown\":false"),
|
||||||
|
json + "{}",
|
||||||
|
json.replace("\"y\":18", "\"y\":18.5")
|
||||||
|
);
|
||||||
|
int index = 0;
|
||||||
|
for (String malformed : bad) {
|
||||||
|
var badDirectory = scratch.resolve("bad-" + index++); Files.createDirectories(badDirectory);
|
||||||
|
var failed = new StructureCensusStore(badDirectory, SEED); Files.writeString(failed.path(), malformed);
|
||||||
|
reject(failed::load, "Malformed structure index");
|
||||||
|
reject(() -> failed.save(snapshot), "Saving after invalid load");
|
||||||
|
require(Files.readString(failed.path()).equals(malformed), "Invalid index must be preserved");
|
||||||
|
}
|
||||||
|
var stale = new StructureCensusStore(directory, SEED); stale.load();
|
||||||
|
Files.writeString(store.path(), "external edit", StandardCharsets.UTF_8);
|
||||||
|
reject(() -> stale.save(new Snapshot(Map.of(), Map.of())), "Concurrent file changes");
|
||||||
|
require(Files.readString(store.path()).equals("external edit"), "Foreign edits are not overwritten");
|
||||||
|
} finally {
|
||||||
|
try (var paths = Files.walk(scratch)) { for (var path : paths.sorted(Comparator.reverseOrder()).toList()) Files.deleteIfExists(path); }
|
||||||
|
}
|
||||||
|
System.out.println("Structure census: native starts and feature origins deduplicate, finite coverage stays explicit, persistence is atomic and corrupt data is preserved.");
|
||||||
|
}
|
||||||
|
private static Site room(String dimension, int x, int y, int z) {
|
||||||
|
return new Site(new SiteKey(dimension, StructureCensusMemory.MONSTER_ROOM, x, y, z, true), List.of(new Bounds(x, y, z, x, y, z)));
|
||||||
|
}
|
||||||
|
private static void priorities(StructureCensusMemory memory) {
|
||||||
|
var complete = new StructureCensusService.Region(OVERWORLD, "complete", 23, "ready", new Coverage(2, 2, 2, 2), Map.of(
|
||||||
|
"camp", new StructureCensusService.FamilyCount(0, 0, StructureCensusService.Presence.ABSENT),
|
||||||
|
"village", new StructureCensusService.FamilyCount(0, 0, StructureCensusService.Presence.ABSENT),
|
||||||
|
"monument", new StructureCensusService.FamilyCount(0, 0, StructureCensusService.Presence.INCOMPATIBLE)));
|
||||||
|
var partial = new StructureCensusService.Region(OVERWORLD, "partial", 23, "ready", new Coverage(4, 1, 1, 1), Map.of(
|
||||||
|
"camp", new StructureCensusService.FamilyCount(0, 0, StructureCensusService.Presence.UNKNOWN)));
|
||||||
|
var view = new StructureCensusService.View(true, false, memory.snapshot(), List.of(complete, partial), List.of());
|
||||||
|
require(StructureCensusService.confirmedPriorities(view, OVERWORLD).equals(List.of("camp", "village")),
|
||||||
|
"Unknown neighbours do not cancel an absence certified in another complete region");
|
||||||
|
var village = new Site(new SiteKey(OVERWORLD, "minecraft:village_plains", 8, 0, 8, false), List.of(new Bounds(128, 40, 128, 140, 60, 140)));
|
||||||
|
var known = new StructureCensusMemory(memory.snapshot()); known.observe(village.key().chunk(), StructureCensusMemory.STARTS, List.of(village));
|
||||||
|
var withPresence = new StructureCensusService.View(true, false, known.snapshot(), view.regions(), List.of());
|
||||||
|
require(StructureCensusService.confirmedPriorities(withPresence, OVERWORLD).equals(List.of("camp")),
|
||||||
|
"A known planned start outside the complete region removes the duplicate-family bonus");
|
||||||
|
require(StructureCensusService.confirmedPriorities(new StructureCensusService.View(true, false, memory.snapshot(), List.of(partial), List.of()), OVERWORLD).isEmpty(),
|
||||||
|
"Partial evidence alone cannot create absence bonuses");
|
||||||
|
require(StructureCensusService.confirmedPriorities(new StructureCensusService.View(false, false, memory.snapshot(), view.regions(), List.of()), OVERWORLD).isEmpty(),
|
||||||
|
"Unavailable derived index means neutral priorities");
|
||||||
|
}
|
||||||
|
@FunctionalInterface private interface Attempt { void run() throws Exception; }
|
||||||
|
private static void reject(Attempt attempt, String message) throws Exception {
|
||||||
|
try { attempt.run(); } catch (IOException | IllegalArgumentException | IllegalStateException | UnsupportedOperationException expected) { return; }
|
||||||
|
throw new AssertionError("Accepted " + message);
|
||||||
|
}
|
||||||
|
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package fr.koka.sanctuary.worldgen.mineral;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.worldgen.city.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.heritage.*;
|
||||||
|
import fr.koka.sanctuary.worldgen.ruins.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
|
||||||
|
/** Positive terrain witnesses plus strict refusal and immutable geometry checks. */
|
||||||
|
public final class Secrets23Smoke {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
|
||||||
|
for (int size : new int[]{512, 724, 1024}) {
|
||||||
|
var terrain = new Plateau();
|
||||||
|
var hall = LostCityUnderground23.plan(42, 0, 0, size, terrain);
|
||||||
|
require(!hall.isEmpty(), "Suitable terrain retains the core hall");
|
||||||
|
require(hall.networks().stream().flatMap(n -> n.modules().stream()).noneMatch(m -> m.kind() == LostCityUnderground16.Kind.POOLROOM), "No selected pool room");
|
||||||
|
for (var n : hall.networks()) for (var m : n.modules()) if (m.kind() == LostCityUnderground16.Kind.ACCESS)
|
||||||
|
require(Math.hypot(m.centerX(), m.centerZ()) >= 56, "Hall entrance preserves natural spawn");
|
||||||
|
var old = LostCityUnderground22.plan(42, 0, 0, size, terrain);
|
||||||
|
require(old.networks().stream().flatMap(n -> n.modules().stream()).anyMatch(m -> m.kind() == LostCityUnderground16.Kind.POOLROOM), "Published22 still retains pool geometry");
|
||||||
|
var ruins = RuinsPlanner23.plan(42, 0, 0, size, terrain);
|
||||||
|
require(ruins.networks().size() == 1 && ruins.networks().getFirst().rooms().size() == 3, "One connected workshop");
|
||||||
|
require(ruins.networks().getFirst().rooms().stream().allMatch(r -> r.kind() == RuinsPlan17.Kind.ACCESS
|
||||||
|
|| r.kind() == RuinsPlan17.Kind.CORRIDOR || r.kind() == RuinsPlan17.Kind.WORKSHOP), "No ordinary historical catalogue");
|
||||||
|
var heritage = HeritagePlanner23.plan(42, 0, 0, size, terrain);
|
||||||
|
require(heritage.sites().size() == 1 && heritage.sites().getFirst().kind() == HeritagePlan19.Kind.OBSERVATORY, "Only observatory survives selection");
|
||||||
|
require(heritage.links().stream().anyMatch(l -> l.kind() == HeritagePlan19.RouteKind.FREIGHT), "Rails have independent natural endpoints");
|
||||||
|
heritage.segments().forEach(s -> s.cells().forEach(c -> require(Math.hypot(c.x(), c.z()) >= 48, "Routes preserve natural spawn")));
|
||||||
|
var mineral = MineralSanctuaryPlanner23.plan(42, 0, 0, size, new MineralPlateau());
|
||||||
|
require(!mineral.isEmpty(), "Suitable rock retains mineral vault");
|
||||||
|
require(mineral.equals(MineralSanctuaryPlanner23.plan(42, 0, 0, size, new MineralPlateau())), "Deterministic mineral geometry and budget");
|
||||||
|
var c = mineral.chamber();
|
||||||
|
require(c.width() == 79 && c.depth() == 73 && c.height() == 27, "Full agreed envelope, no reduced fallback");
|
||||||
|
require(mineral.samples() <= MineralSanctuaryPlanner23.MAX_SAMPLES && mineral.columns() <= MineralSanctuaryPlanner23.MAX_COLUMNS, "Bounded search");
|
||||||
|
for (int face = 0; face < 4; face++) {
|
||||||
|
var facing = new MineralSanctuaryPlan23.Chamber(c.minX(), c.minY(), c.minZ(), c.width(), c.height(), c.depth(), face, c.seed());
|
||||||
|
require(MineralSanctuaryPiece23.decode(MineralSanctuaryPiece23.encode(facing)).equals(facing), "Saved orientation and geometry roundtrip");
|
||||||
|
var invalid = MineralSanctuaryPiece23.encode(facing); invalid.putInt("Width", 63);
|
||||||
|
try { MineralSanctuaryPiece23.decode(invalid); throw new AssertionError("Reduced saved envelope accepted"); }
|
||||||
|
catch (IllegalArgumentException expected) { /* Stable23 shape is mandatory. */ }
|
||||||
|
render(facing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fixture = new MineralSanctuaryPlan23.Chamber(80, 174, 80, 79, 27, 73, 1, 2);
|
||||||
|
var exposed = new Plateau() {
|
||||||
|
@Override public int surfaceY(int x, int z) { return x >= fixture.maxX() - 7 ? -1 : 220; }
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return x < fixture.maxX() - 7 && super.solid(x, y, z); }
|
||||||
|
};
|
||||||
|
require(MineralSanctuaryPlanner23.admits(fixture, exposed), "Attached exposed flank need not have native rock under every column");
|
||||||
|
var pocket = new Plateau() {
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
boolean approach = x > fixture.maxX() && y >= 178 && y <= 195;
|
||||||
|
boolean hole = Math.abs(x - fixture.centerX()) <= 3 && Math.abs(z - fixture.centerZ()) <= 3 && y >= 170 && y <= 173;
|
||||||
|
return !approach && !hole && super.solid(x, y, z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
require(MineralSanctuaryPlanner23.admits(fixture, pocket), "A small existing pocket under the sole remains supported nearby");
|
||||||
|
var largeGap = new Plateau() {
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
boolean approach = x > fixture.maxX() && y >= 178 && y <= 195;
|
||||||
|
boolean hole = Math.abs(x - fixture.centerX()) <= 14 && Math.abs(z - fixture.centerZ()) <= 14 && y >= 170 && y <= 173;
|
||||||
|
return !approach && !hole && super.solid(x, y, z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
require(!MineralSanctuaryPlanner23.admits(fixture, largeGap), "A wide unsupported gap still refuses the complete structure");
|
||||||
|
var protectedFlank = new Plateau() {
|
||||||
|
@Override public int surfaceY(int x, int z) { return exposed.surfaceY(x, z); }
|
||||||
|
@Override public boolean solid(int x, int y, int z) { return exposed.solid(x, y, z); }
|
||||||
|
@Override public boolean protectedAt(int x, int y, int z) { return x == fixture.centerX() && z == fixture.centerZ() && y == 185; }
|
||||||
|
};
|
||||||
|
require(!MineralSanctuaryPlanner23.admits(fixture, protectedFlank), "Even one protected water/structure point prevents admission");
|
||||||
|
var deepRare = render(new MineralSanctuaryPlan23.Chamber(80, 60, 80, 79, 27, 73, 0, 2));
|
||||||
|
var deepOrdinary = render(new MineralSanctuaryPlan23.Chamber(80, 60, 80, 79, 27, 73, 0, 0));
|
||||||
|
require(deepRare.values().stream().anyMatch(s -> s.is(Blocks.DIAMOND_ORE)), "Seeded deep pocket can expose rare resources");
|
||||||
|
require(deepOrdinary.values().stream().noneMatch(s -> s.is(Blocks.DIAMOND_ORE) || s.is(Blocks.GOLD_ORE)
|
||||||
|
|| s.is(Blocks.REDSTONE_ORE) || s.is(Blocks.EMERALD_ORE)), "Rare resources remain optional");
|
||||||
|
var protectedTerrain = new Plateau() { @Override public boolean protectedAt(int x, int y, int z) { return true; } };
|
||||||
|
require(MineralSanctuaryPlanner23.plan(42, 0, 0, 724, protectedTerrain).isEmpty(), "Never overwrite protected hydrology");
|
||||||
|
require(MineralSanctuaryPlanner23.plan(42, 0, 0, 724, new Plateau()).isEmpty(), "Sealed mineral vault has no discoverable approach");
|
||||||
|
var voidTerrain = new Plateau() { @Override public boolean solid(int x, int y, int z) { return false; } };
|
||||||
|
require(MineralSanctuaryPlanner23.plan(42, 0, 0, 724, voidTerrain).isEmpty(), "Never float a disconnected mineral vault");
|
||||||
|
var noBuildings = new Plateau() { @Override public boolean protectedAt(int x, int y, int z) { return y >= 228; } };
|
||||||
|
var rails = HeritagePlanner23.plan(42, 0, 0, 724, noBuildings);
|
||||||
|
require(rails.sites().isEmpty() && !rails.links().isEmpty(), "Rails survive rejection of every optional building");
|
||||||
|
System.out.println("Secrets23Smoke: full three-size vault, four orientations,39-square clear centre, native geometry, optional selection and preserved22 passed.");
|
||||||
|
}
|
||||||
|
private static Map<BlockPos, BlockState> render(MineralSanctuaryPlan23.Chamber c) {
|
||||||
|
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||||
|
MineralSanctuaryRenderer23.render(c, new LostCityRenderer.BlockWriter() {
|
||||||
|
@Override public void set(int x, int y, int z, BlockState state) {
|
||||||
|
require(x >= 0 && x < c.width() && y >= 0 && y < c.height() && z >= 0 && z < c.depth(), "Renderer stays in saved native envelope");
|
||||||
|
blocks.put(new BlockPos(x, y, z), state);
|
||||||
|
}
|
||||||
|
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { throw new AssertionError("No man-made loot"); }
|
||||||
|
});
|
||||||
|
for (int z = c.depth() / 2 - 19; z <= c.depth() / 2 + 19; z++)
|
||||||
|
for (int x = c.width() / 2 - 19; x <= c.width() / 2 + 19; x++) {
|
||||||
|
require(blocks.get(new BlockPos(x, 2, z)).isSolid(), "Full39x39 central floor");
|
||||||
|
for (int y = 3; y <= 7; y++) require(blocks.get(new BlockPos(x, y, z)).isAir(), "Clear central area for future encounter");
|
||||||
|
}
|
||||||
|
require(blocks.values().stream().anyMatch(s -> s.is(Blocks.STONE_STAIRS) || s.is(Blocks.COBBLESTONE_STAIRS)), "Supported terrace stairs appear");
|
||||||
|
Map<BlockPos, BlockState> clipped = new HashMap<>();
|
||||||
|
LostCityRenderer.BlockWriter writer = new LostCityRenderer.BlockWriter() {
|
||||||
|
public void set(int x, int y, int z, BlockState state) { clipped.put(new BlockPos(x, y, z), state); }
|
||||||
|
public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { throw new AssertionError(); }
|
||||||
|
};
|
||||||
|
for (int z = 0; z < c.depth(); z += 16) for (int x = 0; x < c.width(); x += 16)
|
||||||
|
MineralSanctuaryRenderer23.render(c, writer, x, z, x + 15, z + 15);
|
||||||
|
require(clipped.equals(blocks), "Independently clipped chunks reconstruct exactly the complete vault");
|
||||||
|
for (var ore : new net.minecraft.world.level.block.Block[]{Blocks.COAL_ORE, Blocks.IRON_ORE, Blocks.COPPER_ORE})
|
||||||
|
require(blocks.values().stream().filter(s -> s.is(ore)).count() > 50, "Visible coherent ore seams");
|
||||||
|
require(blocks.values().stream().noneMatch(s -> s.is(Blocks.STONE_BRICKS) || s.is(Blocks.CHEST)), "Natural palette only");
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
private static class Plateau implements HeritagePlanner21.Terrain, RuinsPlanner17.Terrain,
|
||||||
|
LostCityUnderground22.Terrain, MineralSanctuaryPlanner23.Terrain {
|
||||||
|
public boolean owns(int x, int z) { return Math.abs(x) < 600 && Math.abs(z) < 600; }
|
||||||
|
public int surfaceY(int x, int z) { return 220; }
|
||||||
|
public boolean solid(int x, int y, int z) { return y >= 20 && y <= 220; }
|
||||||
|
public boolean protectedAt(int x, int y, int z) { return false; }
|
||||||
|
public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return y > 220; }
|
||||||
|
}
|
||||||
|
private static class MineralPlateau extends Plateau {
|
||||||
|
@Override public boolean solid(int x, int y, int z) {
|
||||||
|
// Long existing lateral fissures, supported by an unbroken rock floor.
|
||||||
|
boolean cave = y >= 178 && y <= 195 && (Math.floorMod(x, 64) < 8 || Math.floorMod(z, 64) < 8);
|
||||||
|
return !cave && super.solid(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void require(boolean yes, String message) { if (!yes) throw new AssertionError(message); }
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "0.1.0-alpha.23"
|
version = "0.1.0-alpha.23.1"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user