feat(worldgen): add persistent aerial Sanctuary sites for alpha.24
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-11 18:29:38 +02:00
parent fbefb2e6d6
commit 27d7cba93f
61 changed files with 3469 additions and 132 deletions
+18
View File
@@ -20,6 +20,24 @@ publiée** : le placement des structures redevient sélectif. Les palettes,
intérieurs et filons de lalpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md) intérieurs et filons de lalpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
et [la distribution](docs/packwiz.md). et [la distribution](docs/packwiz.md).
## Alpha.24 — les lieux suspendus, livraison locale vérifiée
La nouvelle génération `sanctuary:island_v24` ajoute une montgolfière, deux
navires marchands habités et deux îlots riches en minerais autour de Sanctuary.
Les îlots reçoivent de lherbe, des fleurs et des petits arbres ; exactement
lun des deux possède une cascade. Chaque lieu contient un coffre, enterré
dans les îlots. Il faut construire pour les atteindre et ramener les villageois.
Les cinq emplacements sont réservés dès la création du monde, même avant leur
exploration. Les expansions respectent ces réservations ; les anciennes
sauvegardes gardent leur génération. Créer un **nouveau monde Sanctuary** pour
les découvrir, avec les structures et loption expérimentale activées.
Les tailles 512 / 724 / 1 024 et le bouton natif **Personnaliser** restent
inchangés. Voir [le contrat](docs/generation-alpha24.md) et
[les vérifications](docs/testing-alpha24.md). Les quatorze scénarios de génération,
les 24 GameTests généraux, `check build assemblePack` et le MRpack passent sur
Mac. La publication puis la synchronisation Prism sont en préparation.
## Alpha.23.1 — livraison locale vérifiée ## 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 [chantier de génération](docs/generation-alpha23.1.md) réunit le Blocodex, une
+11
View File
@@ -941,3 +941,14 @@ Deep Dark et cités anciennes attendent les Cavernes.
Les [preuves de validation](testing-alpha23.1.md) séparent intentions, tests 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. exécutés et état de la distribution. Aucun boss, portail actif ou raid livré ici.
### WG-28 — Les lieux suspendus de Sanctuary — alpha.24
Branche `codex/aerial-sanctuary-alpha24`. [Contrat](generation-alpha24.md) :
une montgolfière, deux navires avec villageois vanilla, deux îlots riches avec
coffres enterrés et exactement une cascade. Sites planifiés avant expansion,
réservations locales, vrais départs de structures dans le vide et scopes
opérateur distincts. Les îlots ont une surface végétalisée, avec petits arbres,
herbes et fleurs. [Validation](testing-alpha24.md) réussie sur Mac : 14 scénarios,
écoulement et admission réels, 24 GameTests généraux, build et MRpack vérifiés.
Publication et synchronisation en préparation.
+90
View File
@@ -0,0 +1,90 @@
# Alpha.24 — les lieux suspendus de Sanctuary
## Contrat
Le nouveau codec `sanctuary:island_v24` ajoute cinq lieux au monde initial :
une montgolfière, deux bateaux de fret et deux îlots minéraux. Ils sont planifiés
à la création et construits seulement lorsque leurs chunks sont générés.
La génération de terrain reste celle de lalpha.23.1 : mêmes trois tailles
512 / 724 / 1 024, Moyen par défaut, bouton Minecraft **Personnaliser**,
secrets minéraux, voies abandonnées, structures vanilla, océans dexpansion,
registre et admission dau moins une structure adaptée par expansion acceptée.
Les cinq sites cherchent la périphérie, hors de lemprise du terrain initial.
Chaque site possède un point de rivage vérifié, à moins de 512 blocs, et un
segment de vue dégagé dans le relief et hors des volumes des secrets déjà
planifiés. Les distances de rendu du client et du
serveur déterminent sa visibilité en jeu ; les arbres ultérieurs peuvent
masquer une partie de cette vue. Le relief nest pas prolongé vers les sites.
Aucun pont, quai, pilier jusquau sol ou accès automatique nest fourni.
## Formes et ressources
La montgolfière possède une enveloppe arrondie de laine claire, un accent,
une nacelle en chêne et des suspentes attachées. Elle reste sous Y350.
Les deux navires ont une coque biseautée, des voiles, un pont protégé,
une cabine et une cale. Le navire de vivres accueille un fermier adulte ;
celui de matériel, un outilleur adulte. Leurs postes de travail sont vanilla.
Il faut construire pour atteindre les lieux et ramener les villageois.
Les îlots ont une roche irrégulière, une base effilée et des filons connectés.
Leur surface reçoit une couverture de terre et dherbe majoritaire, des fleurs,
des herbes et quelques petits chênes ou bouleaux. Le volume réservé comprend
les couronnes ; l’épaisseur rocheuse reste de 20 à 35 blocs.
Leur profondeur relative détermine les minerais : charbon, fer et cuivre
abondants ; poches dor, lapis, redstone, diamant et émeraude. Ces réserves
sont finies. Le recensement existant compte les blocs dans les chunks
observés, sans charger toute la région ni lire le butin encore fermé.
Exactement un îlot possède une source deau qui s’écoule suivant les règles
Minecraft et peut tomber dans le vide.
Chaque lieu contient exactement un coffre dédié. Ceux des îlots sont enterrés
sous une couverture stable, à l’écart de la cascade. Les tables de butin
utilisent exclusivement des objets vanilla, sans récompense future ni Anomaly.
Ni le butin, ni les habitants, ni les blocs détruits ne se renouvellent au
rechargement dun chunk déjà généré.
## Persistance et expansions
La recherche est déterministe, limitée à 64 candidats par site et 8 192
colonnes de terrain mises en cache pour lensemble du plan. Les plans des
secrets racine sont calculés auparavant, puis réutilisés au démarrage ; une
voûte ou une galerie ne peut ainsi recouvrir le point côtier certifié. Cette
planification ne demande aucun chunk. Un échec interrompt explicitement la création du plan plutôt que
de livrer un monde avec un satellite silencieusement manquant.
Le manifeste immuable est écrit dans `data/sanctuary-aerial-v24/` avant le
premier journal `data/sanctuary-world-v24/`. Son SHA-256 est lié au journal24.
Un rechargement relit le plan stocké ; il ne le recalcule pas. Un manifeste
manquant, altéré ou incompatible bloque la reprise avant toute expansion.
Ce contrat ne sapplique quaux nouveaux mondes24 ; les anciens journaux,
codecs, profils et terrains restent lisibles sans migration.
Les réservations couvrent exactement les chunks des cinq structures et un
petit masque local autour de la sortie deau, sans marge sur tout le pourtour
des sites. Elles ne deviennent pas des propriétaires de terrain :
les biomes du vide et le bruit de génération restent inchangés. Ladmission
dexpansion, le journal et la reprise de vide vérifient ces emprises même si
aucun joueur na encore exploré le site. Les passages entre elles restent libres.
Les départs et pièces sont natifs, enregistrés sous `aerial_balloon`,
`aerial_merchant_ship` et `aerial_mineral_islet`. Les écritures sont limitées
au site et au chunk courant. Le registre distingue les sites planifiés,
les départs acceptés et la génération observée. Les scopes aériens ne servent
pas à conclure quune famille de structures vanilla manque au serveur.
`/locate structure` et `/sanctuary census structures aerial [site]` les
référencent sans ouvrir dexpansion ni transformer une consultation en visite.
Les options **Structures expérimentales** et **Générer les structures**
contrôlent les satellites. Leur désactivation produit un plan vide, sans
réservations aériennes. Les options sont sauvegardées avec le monde.
## Limites de cette livraison
Minecraft reste en 26.3-pre-2. Les nuages et la hauteur du monde sont inchangés.
Aucun véhicule pilotable, bateau-poule, temple, commerce Sanctuary, boss,
portail des Cavernes ou raid nest ajouté. Deep Dark et cités anciennes
restent réservés aux futures Cavernes.
Les vérifications réellement exécutées, les témoins et l’état de distribution
figurent dans [testing-alpha24.md](testing-alpha24.md).
+19
View File
@@ -13,6 +13,25 @@ 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.24 locale vérifiée — 11 septembre 2026
`check build assemblePack` et les essais natifs passent. Le JAR et
`build/Sanctuary-0.1.0-alpha.24.mrpack` sont vérifiés : versions, index,
empreintes et JAR intégré concordent. Minecraft 26.3-pre-2, Fabric Loader
0.19.5 et Fabric API 0.160.0+26.3 sont conservés. Voir
[les vérifications](testing-alpha24.md) et [la génération](generation-alpha24.md).
SHA-256 du JAR :
```text
f045655cef30bbaa93099a24443462f213d7851fecd03b7b00c6ca874c9d9c45
```
Laccès Git est rétabli. Les artefacts sont prêts ; leur publication, lavance
du canal puis les deux synchronisations isolées et celles de linstance
Prism existante restent à effectuer. Le ZIP damorçage suit le canal stable ;
il ne sert pas à réimporter linstance existante.
## Alpha.23.1 locale vérifiée — 11 septembre 2026 ## 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 Le JAR et `build/Sanctuary-0.1.0-alpha.23.1.mrpack` sont construits et vérifiés
+262
View File
@@ -0,0 +1,262 @@
# Vérifications alpha.24
Branche `codex/aerial-sanctuary-alpha24`, base alpha.23.1 `fbefb2e`.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
Java 25 ; mod et pack `0.1.0-alpha.24`.
**Matrice de génération réussie le 11 septembre 2026.** Les quatorze scénarios
passent : neuf créations, deux réouvertures, deux options OFF et lordre inverse
de chargement. Les tests purs, deux admissions réelles dexpansion et la
cascade native jusqu’à Y8 réussissent également. Les 24 GameTests généraux passent également.
`check build assemblePack` passe en 3 min 56 s (83 tâches), avec les 24
GameTests natifs également réussis dans ce build. JAR, MRpack, index packwiz
et bootstrap officiel sont vérifiés. La publication et la synchronisation
restent en préparation.
Le [contrat de génération](generation-alpha24.md) décrit le périmètre.
## Environnement et preuves
Les essais utilisent un serveur Fabric GameTest sans interface graphique sur
Mac. Les mondes jetables restent dans `mods/sanctuary/build/run/gameTest/` ;
aucune sauvegarde personnelle nest ouverte ou modifiée. Aucun essai Windows
natif ni contrôle graphique du client avec shaders na été exécuté.
Les résultats retenus figurent dans `build/alpha24-evidence/matrix.json`,
avec les rapports `<phase>-<graine>-<diamètre>.json` et leurs journaux `.log`.
Un résultat nest retenu quaprès sortie réussie du processus et
`passed: true` dans le rapport. Ces fichiers de développement sont ignorés par
Git. Les essais précédant la végétation sont conservés séparément dans
`build/alpha24-evidence-before-vegetation/` et ne valident pas les formes finales.
Ceux précédant la correction du rivage sont dans
`build/alpha24-evidence-before-shore-fix/` ; ils attestent les contrôles déjà
réussis, mais pas les nouveaux emplacements recherchés par le filtre corrigé.
Le journal `build/alpha24-shore-smoke.log` atteste les tests purs finaux.
Les images `balloon.png`, `merchant_*.png` et `mineral_islet_*.png` sont des
projections de contrôle calculées depuis lexport des blocs du renderer :
ce ne sont pas des captures d’écran Minecraft. Le test FULL vérifie séparément
que les matériaux prévus existent dans le monde natif.
## Résultats exécutés
| Essai | Résultat | Preuve |
| --- | --- | --- |
| Placement pur : 3 tailles × graines 0, 42, 2026 ; rotations, emprises, conflits et rivage obstrué | Réussi après correction | `alpha24-shore-smoke.log`, `Aerial24Smoke` |
| Manifeste avant journal, relecture stricte, corruption, absence, conflits, plan vide et historique23 | Réussi | Même journal, `AerialPersistence24Smoke` |
| Neuf créations de la matrice après correction du rivage | 9 sur 9 réussies | Tableau ci-dessous |
| Moyen 724, graine 0, première réouverture | Réussi, 54,85 s | `reload1-0-724.json` |
| Même monde, seconde réouverture | Réussi, 54,50 s | `reload2-0-724.json` |
| Option expérimentale OFF, en serveur natif | Réussi, 52,93 s | `create-0-724-experimental-off.json` |
| Structures globales OFF, en serveur natif | Réussi, 27,19 s | `create-0-724-global-off.json` |
| Génération des chunks dans lordre inverse | Réussi, 81,58 s ; sept champs strictement identiques | `create-0-724-reverse.json`, `reverse-comparison.json` |
| Cascade jusqu’à Y8 et deux admissions réelles entre les emprises | Réussi, 2 min 39 s | `flow-admission-0-724.json`, `alpha24-flow-admission.log` |
| Comparaison du témoin historique23 avec lalpha.23.1 | Réussie sur le code24 avant la passe végétale | `legacy23-comparison.json` |
| GameTests généraux : Blocodex, options, population et hydrologie | 24 sur 24 réussis, JVM 2 min 2 s ; suite 54,47 s | `build/alpha24-general-gametest.log` |
| `check build assemblePack`, contrôle des artefacts | Réussi ; 83 tâches, suite native 24/24 | `build/alpha24-check-build-pack.log`, `build/alpha24-artifacts.json` |
| Graine | Petit 512 | Moyen 724 | Grand 1 024 |
| --- | --- | --- | --- |
| 0 | Réussi, 62,64 s ; 57 chunks FULL | Réussi, 80,96 s ; 61 chunks FULL | Réussi, 128,84 s ; 48 chunks FULL |
| 42 | Réussi, 58,25 s ; 61 chunks FULL | Réussi, 72,91 s ; 57 chunks FULL | Réussi, 92,08 s ; 58 chunks FULL |
| 2026 | Réussi, 59,43 s ; 44 chunks FULL | Réussi, 80,77 s ; 37 chunks FULL | Réussi, 115,93 s ; 48 chunks FULL |
Chaque création active retrouve les cinq sites, leurs cinq coffres, deux
marchands et une source de cascade. Les emprises réservées vont de 31 à 55
chunks selon le monde. Les deux cas OFF retrouvent un plan vide, aucune
réservation aérienne et aucun site aérien annoncé dans le registre.
Lordre inverse, exécuté sur un nouveau Moyen, graine 0, conserve exactement
la géométrie, le codec, les départs NBT, les blocs avant et après usage,
les mutations du test et la végétation. `reverse-comparison.json` consigne
l’égalité stricte de ces sept champs ; les durées et états transitoires de
chargement ne sont pas comparés comme sils devaient être identiques.
Les durées incluent Gradle, le lancement du serveur, la préparation du monde,
les chunks FULL demandés et les assertions. Ce ne sont ni des temps de rendu
client, ni une mesure Windows, ni le délai nécessaire pour générer toute l’île.
### Correction du point de vue de rivage
La première matrice végétalisée avait échoué sur le Petit, graine 42 : le
sanctuaire minéral construisait sa voûte jusqu’à Y187 au-dessus dun point
calculé sur le terrain brut à Y174. Le planner prépare désormais les plans
racine avant le manifeste aérien, puis refuse leurs volumes au niveau du
support, du point de vue et de la ligne de visée. Les assertions FULL restent
strictes ; le cas fautif passe dans la nouvelle série. Deux tests purs
vérifient une obstruction au rivage et au milieu du segment de vue.
Cette correction peut changer le candidat retenu pour un nouveau monde24.
Un plan déjà sauvegardé reste relu tel quel : aucun recalcul implicite et
aucune modification des anciens mondes23.
### Ce que le témoin Moyen vérifie
Les cinq sites réservent **55 chunks**, sans réserver un anneau entier. Le test
charge **61 chunks FULL**, comprenant ces emprises, leurs points de rivage et
le chunk qui reçoit les villageois extraits. Il vérifie notamment :
- Les cinq vrais départs de structures natifs, leurs pièces, leurs coordonnées,
la sérialisation NBT et les états doccupation conservés.
- Cinq coffres réellement placés, leurs tables vanilla et un butin non vide,
puis leur maintien à vide après deux redémarrages.
- Deux villageois présents, un par navire, puis déplacés dans le chunk de test.
Leur position reste conservée et aucun remplaçant napparaît dans les navires.
- Des blocs de minerai et un élément de nacelle retirés restent absents après
les deux réouvertures. Cette manipulation est limitée au monde de développement.
- Des accès et intérieurs avec un sol et deux blocs libres, ainsi que les trois
couches de couverture des coffres enterrés lors de la création.
- Les points de rivage reposent sur le terrain FULL et ne sont pas enfouis.
Les colonnes extérieures échantillonnées restent vides de construction.
- Le registre passe de `PLANNED` à `GENERATED`, les recherches utilisent les
vrais départs et le journal dexpansion reste inchangé pendant ces visites.
La source deau est présente et conservée ; lessai séparé ci-dessous vérifie
sa descente par les ticks de fluide jusqu’à Y8.
La création et les deux réouvertures contrôlent aussi la végétation réelle. Sur l’îlot avec
cascade, **596 des 783 colonnes de surface intactes portent de lherbe**
(76,1 %), avec deux arbres et 27 blocs de hauteur rocheuse. Sur l’îlot sec,
ce sont **1 506 sur 1 788** (84,2 %), quatre arbres et 21 blocs de roche.
Chaque enveloppe ajoute huit blocs pour les couronnes. Les troncs ont un appui
en terre, les couronnes restent dans lemprise et les feuilles utilisent les
propriétés natives de distance, sans persistance forcée.
`persistence-comparison.json` confirme également l’égalité de la géométrie,
du codec, des départs natifs, des blocs après usage, des mutations et des
mesures de végétation dans les deux relectures.
### Admission réelle et écoulement natif
`flow-admission-0-724.json` et `build/alpha24-flow-admission.log` attestent le
test dans un autre monde jetable Moyen, graine 0. Il appelle le véritable
`ExpansionRuntime.Session.create` avant que les satellites soient visités.
| Expansion de test, diamètre 128 | Premier centre envisagé X Z | Centre réellement réservé X Z | Appel de réservation | Résultat |
| --- | --- | --- | ---: | --- |
| `aerial_collision_0_southeast` | `496 336`, chevauche un satellite | `368 496` | 114 ms | `READY`, un départ vanilla valide |
| `aerial_interval_0_northeast` | `400 -432`, passage libre | `400 -432` | 54 ms | `READY`, un départ vanilla valide |
La première demande filtre le candidat en conflit, puis poursuit les
vérifications dadaptation avant sa réservation définitive. La seconde
montre que la bande entre les satellites reste utilisable. Les deux
réservations sont idempotentes, respectent leurs emprises et atteignent
`READY` avec une structure native adaptée. Ces durées dappels ne représentent
pas la préparation complète, comprise dans le test de 2 min 39 s.
Après chargement et activation des ticks des chunks concernés, lunique
source prévue produit de leau sous l’îlot à `399 194 312`, puis à
`399 8 312`, après **1 180 ticks dattente contrôlée**. À Y8, le fluide
est de leau courante, pas une source artificiellement posée. Aucun chemin
vertical de blocs sources nest fabriqué par le renderer. Cet essai valide
ce trajet réel ; il ne garantit pas la forme de l’écoulement après des
constructions ou excavations effectuées par les joueurs.
### Chargement des habitants dans le test Grand
Le premier essai Grand, graine 0, a rencontré un délai de visibilité des
entités : les marchands existaient sur disque mais n’étaient pas encore
exposés au test. Le protocole de lecture charge désormais les navires entiers,
vérifie la disponibilité native des entités et attend au plus 600 ticks.
Lexigence reste de deux marchands distincts, sans création ou remplacement
par le test. Les trois Grands passent ensuite : le témoin graine0 attend
250 ticks et celui de graine2026, 323 ticks, avant de voir les deux habitants.
Cela confirme que lancienne limite de 200 ticks du fixture était trop courte.
Le premier essai interrompu nest pas compté dans les réussites de la matrice.
## Repères de visite — Moyen 724, graine 0
Créer un **nouveau monde Sanctuary de génération24**, taille Moyen, graine `0`,
avec **Générer les structures** et **Structures expérimentales** activés.
Les coordonnées proviennent de `create-0-724.json`, champ `look_views`.
Le nouveau rapport après correction du rivage confirme ces mêmes coordonnées.
Les repères de rivage désignent un point de vue deux blocs au-dessus du témoin
enregistré ; ils ne constituent pas des téléportations de survie exécutées.
| Lieu | Entrée X Y Z | Point de vue au rivage X Y Z | Distance horizontale entréerivage |
| --- | --- | --- | ---: |
| Montgolfière | `-492 288 110` | `-251 219 55` | 247,2 blocs |
| Navire de vivres | `-35 186 -503` | `-19 176 -282` | 221,6 blocs |
| Navire de matériel | `463 186 -204` | `229 159 -102` | 255,3 blocs |
| Îlot avec cascade | `399 237 333` | `195 161 163` | 265,5 blocs |
| Îlot sec | `-277 256 456` | `-125 227 207` | 291,7 blocs |
Pour un repérage de développement, `/locate structure sanctuary:aerial_balloon`,
`sanctuary:aerial_merchant_ship` et `sanctuary:aerial_mineral_islet` utilisent
les types natifs. `/sanctuary census structures aerial` distingue les cinq
sites. Aucun pont ou véhicule nest ajouté pour les atteindre. La limite de
512 blocs depuis un rivage ne force pas les réglages de rendu du client ou
du serveur, ni la visibilité à travers les feuillages.
## Historique23
Le rapport `legacy23-0-724.json`, produit sur un monde de génération23 avec le
code24 avant lajout de végétation aérienne, a été comparé à
`build/alpha231-evidence/final-medium.json`. Tous les champs sont identiques,
à lexception attendue de `process_id` : codec, plans, départs natifs,
géométrie, rails et traversées contrôlés dans **85 chunks FULL**.
`legacy23-comparison.json` consigne cette égalité.
Cette preuve porte sur ce témoin précis, pas sur toutes les graines ni toutes
les dimensions. Les tests de persistance conservent aussi les octets du journal23
et refusent une conversion implicite vers24. Aucun monde existant ne reçoit
automatiquement les satellites24 ; le nouveau manifeste appartient uniquement
aux nouveaux mondes24.
## Reproduction
Depuis la racine du dépôt, exécuter les commandes **séquentiellement**.
`runGameTest` recrée son monde de développement par défaut. Pour les phases
de réouverture, garder le même dossier et ajouter impérativement
`sanctuaryExpansionReload=true`.
```sh
./gradlew :sanctuary:aerial24Smoke :sanctuary:aerialPersistence24Smoke
./gradlew :sanctuary:runGameTest \
-PsanctuaryAerial24Tests=true \
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
-PsanctuaryAerial24Phase=create -PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest \
-PsanctuaryAerial24Tests=true \
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
-PsanctuaryAerial24Phase=reload1 -PsanctuaryExpansionReload=true \
-PsanctuaryDensityMaps=false
./gradlew :sanctuary:runGameTest \
-PsanctuaryAerial24Tests=true \
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
-PsanctuaryAerial24Phase=reload2 -PsanctuaryExpansionReload=true \
-PsanctuaryDensityMaps=false
```
La matrice de créations reprend les diamètres `512`, `724`, `1024` et les
graines `0`, `42`, `2026`. Les variantes du Moyen, graine0, ajoutent séparément
`-PsanctuaryExperimentalStructures=false`,
`-PsanctuaryGenerateStructures=false` ou
`-PsanctuaryAerial24ReverseChunks=true` à la commande de création.
Lessai dadmission et deau utilise un nouveau monde :
```sh
./gradlew :sanctuary:runGameTest \
-PsanctuaryAerial24FlowAdmission=true \
-PsanctuaryTestGeneration=24 -PsanctuaryTestDiameter=724 \
-PsanctuaryTestSeed=0 -PsanctuaryDensityMaps=false
```
Cette dernière commande reproduit lessai dadmission et d’écoulement réussi.
Les assertions sont dans
[Aerial24WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java)
et [Aerial24FlowAdmissionGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java).
## Limites et distribution
Les essais ne valident pas encore le ressenti en vol, la lisibilité dans le
client, l’équilibrage économique des filons, les performances multijoueurs ou
les incidents Windows rapportés sur cette préversion Minecraft. Le test pur
des neuf plans emploie un terrain synthétique ; seule la matrice FULL valide
leur adaptation au relief réel des graines retenues.
Laccès Gitea est rétabli, avec la permission de pousser. La distribution
est en préparation ; la publication et la synchronisation Prism ne sont pas
encore effectuées. L’état final des artefacts et du canal sera consigné après
les contrôles de livraison.
+2 -2
View File
@@ -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.1 mod_version=0.1.0-alpha.24
pack_version=0.1.0-alpha.23.1 pack_version=0.1.0-alpha.24
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
+27 -2
View File
@@ -40,7 +40,11 @@ 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' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : '23')) providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : '24'))
systemProperty('sanctuary.test.aerial24.flowAdmission', providers.gradleProperty('sanctuaryAerial24FlowAdmission').getOrElse('false'))
systemProperty('sanctuary.test.aerial24', providers.gradleProperty('sanctuaryAerial24Tests').getOrElse('false'))
systemProperty('sanctuary.test.aerial24.reverseChunks', providers.gradleProperty('sanctuaryAerial24ReverseChunks').getOrElse('false'))
systemProperty('sanctuary.test.aerial24.phase', providers.gradleProperty('sanctuaryAerial24Phase').getOrElse('create'))
systemProperty('sanctuary.test.secrets23', providers.gradleProperty('sanctuarySecrets23Tests').getOrElse('false')) systemProperty('sanctuary.test.secrets23', providers.gradleProperty('sanctuarySecrets23Tests').getOrElse('false'))
systemProperty('sanctuary.test.admission23', providers.gradleProperty('sanctuaryAdmission23Tests').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.reserveOnly', providers.gradleProperty('sanctuaryAdmission23ReserveOnly').getOrElse('false'))
@@ -94,6 +98,10 @@ 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 aerial24Flow = providers.gradleProperty('sanctuaryAerial24FlowAdmission').map { it.toBoolean() }.getOrElse(false)
inputs.property('aerial24Flow', aerial24Flow)
def aerial24 = providers.gradleProperty('sanctuaryAerial24Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('aerial24', aerial24)
def secrets23 = providers.gradleProperty('sanctuarySecrets23Tests').map { it.toBoolean() }.getOrElse(false) def secrets23 = providers.gradleProperty('sanctuarySecrets23Tests').map { it.toBoolean() }.getOrElse(false)
def admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false) def admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false)
inputs.property('admission23', admission23) inputs.property('admission23', admission23)
@@ -111,7 +119,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: admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests' ? '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')
@@ -588,3 +596,20 @@ tasks.named('check') { dependsOn('expansionOcean23Smoke') }
} }
tasks.named('check') { dependsOn(taskName) } tasks.named('check') { dependsOn(taskName) }
} }
// Versioned aerial planner: bounded search, native pieces and local claims.
tasks.register('aerial24Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
}
tasks.named('check') { dependsOn('aerial24Smoke') }
tasks.register('aerialPersistence24Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
}
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
@@ -0,0 +1,179 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
/** Separate disposable world: real admission before visiting satellites, then an actual ticking waterfall. */
public final class Aerial24FlowAdmissionGameTests {
private record Request(String id, String direction, int diameter, ExpansionIsland raw, ExpansionIsland filtered) {}
@GameTest(maxTicks = 24000)
public void reservedSatellitesAndNativeWaterfall(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 24 && generator.experimentalStructures()
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
var plan = generator.aerialPlan24();
var session = Objects.requireNonNull(ExpansionRuntime.session(level));
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 5, "Use an untouched disposable world for admission witnesses");
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
String dimension = level.dimension().identifier().toString();
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
helper.assertTrue((evidence.chunks().getOrDefault(new StructureCensusMemory.ChunkKey(dimension, chunk.x(), chunk.z()), 0) & StructureCensusMemory.FULL) == 0,
"Satellite claims are protected before any of their chunks reach FULL");
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter()); report.put("process_id", ProcessHandle.current().pid());
report.put("unvisited_claims_verified", true); report.put("sites", plan.sites());
var admitted = new ArrayList<ExpansionIsland>();
try {
report.put("stage", "real admission toward an unseen satellite");
admitted.add(admit(helper, level, generator, session, plan, true, report));
report.put("stage", "real admission through an unclaimed interval");
admitted.add(admit(helper, level, generator, session, plan, false, report));
} catch (Exception error) { failure(report, error); throw error; }
var waterfall = plan.sites().stream().filter(AerialPlan24.Site::waterfall).findFirst().orElseThrow();
var requested = waterfall.claimedChunks(); var iterator = requested.iterator();
boolean[] tickets = {false}; int[] flowTicks = {0};
Runnable release = () -> {
if (tickets[0]) requested.forEach(chunk -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, chunk, 2));
tickets[0] = false;
};
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (admitted.stream().anyMatch(island -> !session.island(island.id()).status().equals("ready"))) {
report.put("stage", "prepare admitted expansions"); report.put("progress", session.progress());
helper.assertTrue(!session.progress().contains("failed") && !session.progress().contains("reload_required")
&& !session.progress().contains("unavailable"), "Real admission preparation progresses without reload or failure");
helper.runAtTickTime(helper.getTick() + 5, () -> step[0].run()); return;
}
if (!report.containsKey("native_ready_starts")) {
var nativeStarts = new TreeMap<String, Integer>();
for (var reserved : admitted) {
var ready = session.island(reserved.id());
helper.assertTrue(ready.equals(reserved.ready()) && !plan.conflicts(ready), "READY retains its immutable safe placement");
int count = 0;
for (var candidate : ExpansionStructures23.candidates(ready)) {
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
count += (int) chunk.getAllStarts().values().stream().filter(StructureStart::isValid)
.filter(start -> ((Object) start) instanceof NativeStructureMetadata23 metadata && metadata.sanctuary$native23() != null).count();
}
helper.assertTrue(count > 0, "Admitted generation24 expansion produces at least one real native start");
nativeStarts.put(ready.id(), count);
}
report.put("native_ready_starts", nativeStarts);
}
if (iterator.hasNext()) {
var chunk = iterator.next(); report.put("stage", "FULL waterfall " + chunk);
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) {
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
}
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks toward the void");
var source = AerialGeometry24.waterfallSource(waterfall);
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
var below = waterAt(level, waterfall, waterfall.minY() - 16);
var low = waterAt(level, waterfall, 8);
report.put("water_below_islet", below == null ? List.of() : coords(below));
report.put("water_near_y0", low == null ? List.of() : coords(low));
if (below == null || low == null) {
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall below the island and down to y=8");
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
}
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
report.put("expansions", admitted.stream().map(island -> session.island(island.id())).toList());
report.put("passed", true);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist the disposable flow/admission witness");
write(report, "full.json"); release.run(); helper.succeed();
} catch (Exception error) { release.run(); failure(report, error); throw new IllegalStateException("Aerial24 flow/admission failed: " + error.getMessage(), error); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static ExpansionIsland admit(GameTestHelper helper, ServerLevel level, SanctuaryChunkGenerator generator,
ExpansionRuntime.Session session, AerialPlan24 plan, boolean collision, Map<String, Object> report) throws Exception {
var requests = requests(level.getSeed(), session.islands(), plan, collision);
helper.assertTrue(!requests.isEmpty(), "The finite candidate search contains a " + (collision ? "satellite overlap" : "free interval") + " witness");
var attempts = new ArrayList<Map<String, Object>>(); report.put(collision ? "collision_attempts" : "interval_attempts", attempts);
for (var request : requests.stream().limit(8).toList()) {
var before = List.copyOf(session.islands());
var filtered = session.candidate(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
helper.assertTrue(filtered.equals(request.filtered()), "Real runtime candidate uses the same immutable aerial claims before any terrain inspection");
if (collision) helper.assertTrue(plan.conflicts(request.raw()) && !plan.conflicts(filtered) && !filtered.equals(request.raw()),
"The first overlapping candidate is rejected before the satellite is visited");
long start = System.nanoTime();
try {
var reserved = session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
long elapsed = (System.nanoTime() - start) / 1_000_000;
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
if (!collision) helper.assertTrue(inAerialBand(reserved, plan), "The admitted free interval lies among the nearby satellites, not beyond a blanket exclusion ring");
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
"Reservation retry is idempotent");
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
return reserved;
} catch (IllegalArgumentException refused) {
helper.assertTrue(before.equals(session.islands()), "Refused native admission writes no reservation");
attempts.add(Map.of("request", request, "refused", String.valueOf(refused.getMessage()), "elapsed_ms", (System.nanoTime() - start) / 1_000_000));
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
}
}
throw new IllegalStateException("No native expansion fits the bounded aerial admission witnesses");
}
private static List<Request> requests(long seed, List<ExpansionIsland> islands, AerialPlan24 plan, boolean collision) {
var result = new ArrayList<Request>();
for (int index = 0; index < 32 && result.size() < 16; index++) for (var direction : ExpansionIsland.DIRECTIONS) {
String id = (collision ? "aerial_collision_" : "aerial_interval_") + index + "_" + direction.replace("_", "");
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
if (filtered == null || !collision && !inAerialBand(filtered, plan)) continue;
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
}
return result;
}
private static boolean inAerialBand(ExpansionIsland island, AerialPlan24 plan) {
double outside = plan.claimsChunks().stream().map(ChunkPos::unpack).mapToDouble(chunk -> Math.hypot(chunk.getMiddleBlockX(), chunk.getMiddleBlockZ()) + 12).max().orElseThrow();
return Math.hypot(island.centerX(), island.centerZ()) - island.reservedRadius() < outside;
}
private static BlockPos waterAt(ServerLevel level, AerialPlan24.Site site, int y) {
for (int x = site.minX() - 3; x <= site.maxX() + 3; x++) for (int z = site.minZ() - 3; z <= site.maxZ() + 3; z++) {
var pos = new BlockPos(x, y, z);
if (level.getFluidState(pos).is(FluidTags.WATER)) return pos;
}
return null;
}
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static void write(Map<String, Object> report, String name) throws java.io.IOException {
var directory = Path.of("diagnostics/aerial24-flow-admission"); Files.createDirectories(directory);
Files.writeString(directory.resolve(name), new GsonBuilder().setPrettyPrinting().create().toJson(report));
}
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("failure", error.toString());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
SanctuaryMod.LOGGER.error("Aerial24 flow/admission failed at {}", report.get("stage"), error);
try { write(report, "failure.json"); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
}
}
@@ -0,0 +1,398 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.*;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** One real world per size/seed. Separate JVM phases preserve looting, mining and extracted occupants. */
public final class Aerial24WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000)
public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; }
}
private static void begin(GameTestHelper helper, Map<String, Object> report) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
helper.assertTrue(generator.rootGeneration() == 24, "Aerial24 requires a disposable generation24 world");
String phase = System.getProperty("sanctuary.test.aerial24.phase", "create");
helper.assertTrue(List.of("create", "reload1", "reload2").contains(phase), "Known aerial lifecycle phase");
boolean create = phase.equals("create");
var plan = generator.aerialPlan24();
report.put("phase", phase); report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("experimental", generator.experimentalStructures());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
helper.assertTrue(plan.isEmpty() && plan.claimsChunks().isEmpty(), "Disabled aerial generation reserves no space");
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
}
helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
if (generator.initialDiameter() == 724 && level.getSeed() == 0)
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
report.put("census_before", StructureCensusService.aerialSites(level));
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
var journalBefore = files(level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()));
verifyClaims(helper, generator, plan, report);
Path world = level.getServer().getWorldPath(LevelResource.ROOT);
JsonObject previous = create ? null : JsonParser.parseString(Files.readString(world.resolve("aerial24-" + (phase.equals("reload1") ? "create" : "reload1") + ".json"))).getAsJsonObject();
if (previous != null) {
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload occurs in a separate JVM after a successful phase");
helper.assertTrue(previous.get("geometry").equals(JSON.toJsonTree(plan.sites())), "Site geometry and palette seeds survive reload");
helper.assertTrue(previous.get("generator_codec").equals(report.get("generator_codec")), "Generator options survive reload");
}
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
plan.sites().forEach(site -> chunks.addAll(site.claimedChunks()));
plan.sites().forEach(site -> chunks.add(ChunkPos.containing(new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ()))));
// The two extracted merchants are retained in a separate, existing root chunk.
chunks.add(new ChunkPos(0, 0));
var loadOrder = new ArrayList<>(chunks);
boolean reverseChunks = Boolean.getBoolean("sanctuary.test.aerial24.reverseChunks");
if (reverseChunks) Collections.reverse(loadOrder);
report.put("reverse_chunks", reverseChunks);
var iterator = loadOrder.iterator();
var entityChunks = new HashSet<ChunkPos>();
plan.sites().stream().filter(AerialPlan24.Site::merchant).forEach(site -> entityChunks.addAll(site.claimedChunks()));
entityChunks.add(new ChunkPos(0, 0));
boolean[] tickets = {false}; int[] wait = {0};
Runnable release = () -> {
if (tickets[0]) entityChunks.forEach(pos -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, pos, 2));
tickets[0] = false;
};
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);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
var occupants = occupants(level, plan);
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
for (var pos : entityChunks) {
boolean loaded = level.areEntitiesLoaded(pos.pack()), ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
readiness.put(pos.toString(), Map.of("loaded", loaded, "ticking", ticking));
entitiesReady &= loaded && ticking;
}
report.put("entity_chunks", readiness);
report.put("occupants_observed", occupants.stream().map(villager -> Map.of("uuid", villager.getUUID().toString(),
"position", List.of(villager.getX(), villager.getY(), villager.getZ()))).toList());
report.put("visible_occupants", occupants.size()); report.put("entity_wait_ticks", wait[0]);
if ((!entitiesReady || occupants.size() < 2) && wait[0]++ < 600) {
report.put("stage", "wait for persistent merchants");
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
helper.assertTrue(entitiesReady, "All ship chunks finish native entity loading and become ticking before occupant inspection");
helper.assertTrue(occupants.size() == 2, "Both one-time merchants are present after native entity loading");
report.put("stage", "inspect native structures");
var starts = starts(helper, level, plan);
var before = inspect(helper, level, plan, create, report);
if (create) {
verifyLocator(helper, level, plan, starts);
report.put("stage", "loot, extract and dismantle disposable witnesses");
report.put("mutations", mutate(helper, level, plan, occupants, before));
} else {
report.put("mutations", previous.get("mutations"));
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
}
var tags = tags(level, starts);
if (!create) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "Native starts, references and occupant flags survive both reloads");
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("blocks_after", countSites(level, plan));
var census = StructureCensusService.aerialSites(level);
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Five historical generated sites remain indexed after loot, mining and demolition");
report.put("census_after", census);
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
report.put("expansion_journal_unchanged", true);
StructureCensusService.flush(level.getServer());
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
report.put("passed", true);
write(world.resolve("aerial24-" + phase + ".json"), report);
write(Path.of("diagnostics/aerial24/" + phase + ".json"), report);
release.run(); helper.succeed();
} catch (Exception error) {
release.run(); failure(report, error); throw new IllegalStateException("Aerial24 failed: " + error.getMessage(), error);
}
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
}
private static void verifyClaims(GameTestHelper helper, SanctuaryChunkGenerator generator, AerialPlan24 plan, Map<String, Object> report) {
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
var views = new LinkedHashMap<String, Object>();
for (var site : plan.sites()) {
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
}
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
"Unclaimed space remains available; there is no blanket exclusion ring");
report.put("look_views", views);
}
private static List<Villager> occupants(ServerLevel level, AerialPlan24 plan) {
var expected = plan.sites().stream().filter(AerialPlan24.Site::merchant).map(AerialGeometry24::occupantUuid).collect(java.util.stream.Collectors.toSet());
return level.getEntitiesOfClass(Villager.class, new AABB(-2048, 0, -2048, 2048, 384, 2048))
.stream().filter(villager -> expected.contains(villager.getUUID())).toList();
}
private static Map<String, StructureStart> starts(GameTestHelper helper, ServerLevel level, AerialPlan24 plan) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var result = new TreeMap<String, StructureStart>();
for (var site : plan.sites()) {
var type = registry.get(Identifier.parse(AerialStructures24.structureId(site))).orElseThrow().value();
var start = level.getChunk(site.startChunkX(), site.startChunkZ(), ChunkStatus.FULL, true).getStartForStructure(type);
helper.assertTrue(start != null && start.isValid(), "Planned aerial site has a real persisted native start: " + site.id());
helper.assertTrue(start.getPieces().stream().filter(piece -> piece instanceof AerialPiece24 aerial && aerial.site().equals(site)).count() == 1,
"Exactly one piece retains the site's immutable descriptor");
var context = StructurePieceSerializationContext.fromLevel(level);
var tag = start.createTag(context, start.getChunkPos());
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, start.getChunkPos())), "Aerial start round-trips through native serialization");
if (site.merchant()) helper.assertTrue(start.getPieces().stream().filter(AerialPiece24.class::isInstance).map(AerialPiece24.class::cast)
.allMatch(AerialPiece24::occupantPlaced), "Merchant placement flag is stored after its first creation");
result.put(site.id(), start);
}
return result;
}
private static Map<String, Map<BlockPos, BlockState>> inspect(GameTestHelper helper, ServerLevel level, AerialPlan24 plan,
boolean fresh, Map<String, Object> report) {
var expected = new LinkedHashMap<String, Map<BlockPos, BlockState>>(); int chestCount = 0, waterSources = 0;
var vegetation = new TreeMap<String, Object>();
var shores = new TreeMap<String, Object>(); report.put("shore_witnesses", shores);
for (var site : plan.sites()) {
var model = render(site); expected.put(site.id(), model);
var shore = new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ());
var column = new TreeMap<Integer, String>();
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
var chestAt = AerialGeometry24.chest(site);
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
if (fresh) {
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
for (var pos : AerialGeometry24.interiorChecks(site)) {
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
vegetation.put(site.id(), vegetation(helper, level, site));
if (site.waterfall()) {
var source = AerialGeometry24.waterfallSource(site);
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
}
// These columns are beyond the authored box and well outside initial terrain ownership.
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
var state = level.getBlockState(pos.atY(y));
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
}
}
helper.assertTrue(chestCount == 5 && waterSources == 1, "Five physical chests and exactly one planned source");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
report.put("vegetation", vegetation);
return expected;
}
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
"Tree headroom preserves the original stone height");
int surface = 0, grass = 0;
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
int top = AerialGeometry24.terrainTop(site, x, z);
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
surface++;
if (level.getBlockState(AerialGeometry24.world(site, x, top, z)).is(Blocks.GRASS_BLOCK)) grass++;
}
helper.assertTrue(grass * 2 > surface, "Most real intact surface columns carry grass: " + site.id());
var trees = AerialGeometry24.trees(site);
helper.assertTrue(!trees.isEmpty(), "At least one real tree on each mineral islet");
for (var tree : trees) {
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
var floor = level.getBlockState(ground);
helper.assertTrue(floor.is(Blocks.GRASS_BLOCK) || floor.is(Blocks.DIRT), "Tree stands on soil, never in the air");
for (int y = 1; y <= tree.trunkHeight(); y++)
helper.assertTrue(level.getBlockState(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Native tree trunk stays continuous");
var crown = ground.above(tree.trunkHeight() + 1);
var leaves = level.getBlockState(crown);
helper.assertTrue(site.bounds().isInside(crown) && leaves.is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES)
&& !leaves.getValue(BlockStateProperties.PERSISTENT) && leaves.getValue(BlockStateProperties.DISTANCE) < 7,
"Complete natural crown survives native chunk decoration and reload");
}
return Map.of("rock_height", site.rockHeight(), "headroom", AerialPlan24.VEGETATION_HEADROOM,
"surface_columns", surface, "grass_columns", grass, "grass_percent", 100.0 * grass / surface, "trees", trees);
}
private static Map<String, Object> mutate(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, List<Villager> occupants,
Map<String, Map<BlockPos, BlockState>> models) {
var emptied = new ArrayList<List<Integer>>(); var removed = new ArrayList<List<Integer>>(); var loot = new TreeMap<String, Integer>();
for (var site : plan.sites()) {
var at = AerialGeometry24.chest(site); var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
chest.unpackLootTable(null); int quantity = 0;
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
}
}
var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(balloon).below();
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
var extracted = new TreeMap<String, List<Double>>(); int i = 0;
for (var villager : occupants.stream().sorted(Comparator.comparing(v -> v.getUUID().toString())).toList()) {
// This fixture parks extracted merchants in an existing root chunk, outside both ships.
villager.setNoAi(true); villager.setNoGravity(true); villager.setDeltaMovement(Vec3.ZERO);
villager.snapTo(4.5 + i++ * 4, 330, 4.5); villager.setPersistenceRequired();
extracted.put(villager.getUUID().toString(), List.of(villager.getX(), villager.getY(), villager.getZ()));
}
return Map.of("emptied_chests", emptied, "removed_blocks", removed, "extracted_merchants", extracted, "loot_items_obtained", loot);
}
private static void verifyMutations(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, JsonObject mutations, List<Villager> occupants) {
for (var value : mutations.getAsJsonArray("removed_blocks")) {
var pos = position(value.getAsJsonArray()); helper.assertTrue(level.getBlockState(pos).isAir(), "Mined ore and dismantled floor remain absent after reload");
}
var extracted = mutations.getAsJsonObject("extracted_merchants");
for (var villager : occupants) {
var at = extracted.getAsJsonArray(villager.getUUID().toString());
helper.assertTrue(at != null && Math.abs(villager.getX() - at.get(0).getAsDouble()) < .01
&& Math.abs(villager.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(villager.getZ() - at.get(2).getAsDouble()) < .01,
"Extracted merchant stays outside its original ship without respawning");
}
for (var site : plan.sites()) if (site.merchant()) helper.assertTrue(level.getEntitiesOfClass(Villager.class, AABB.of(site.bounds())).isEmpty(),
"The visited ship does not replenish an extracted merchant");
}
private static void verifyLocator(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, Map<String, StructureStart> starts) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var holders = new ArrayList<Holder<Structure>>();
plan.sites().stream().map(AerialStructures24::structureId).distinct().forEach(id -> holders.add(registry.get(Identifier.parse(id)).orElseThrow()));
holders.add(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow());
var wanted = HolderSet.direct(holders); var origin = plan.sites().getFirst().locator();
var activeBefore = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands();
int before = starts.values().stream().mapToInt(StructureStart::getReferences).sum();
var found = ExpansionStructures23.findNearest(level, wanted, origin, 100, false);
helper.assertTrue(found != null, "Mixed vanilla/aerial locator finds a real accepted start");
helper.assertTrue(starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before, "Ordinary locate never marks a site referenced");
var referenced = ExpansionStructures23.findNearest(level, wanted, origin, 100, true);
helper.assertTrue(referenced != null && starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before + 1,
"Mixed map lookup references exactly its one winning aerial start");
helper.assertTrue(activeBefore.equals(((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands()), "Locator never activates an expansion");
}
private static Map<BlockPos, BlockState> render(AerialPlan24.Site site) {
var result = new HashMap<BlockPos, BlockState>();
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
});
return result;
}
private static Map<String, Map<String, Integer>> countSites(ServerLevel level, AerialPlan24 plan) {
var all = new TreeMap<String, Map<String, Integer>>();
for (var site : plan.sites()) {
var counts = new TreeMap<String, Integer>();
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) for (int y = site.minY(); y <= site.maxY(); y++) {
var state = level.getBlockState(new BlockPos(x, y, z));
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
}
all.put(site.id(), counts);
}
return all;
}
private static boolean ore(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); }
private static Map<String, String> tags(ServerLevel level, Map<String, StructureStart> starts) {
var result = new TreeMap<String, String>(); var context = StructurePieceSerializationContext.fromLevel(level);
starts.forEach((id, start) -> result.put(id, start.createTag(context, start.getChunkPos()).toString())); return result;
}
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
private static BlockPos position(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
var found = new TreeMap<String, byte[]>();
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
for (var path : paths.filter(Files::isRegularFile).toList()) found.put(directory.relativize(path).toString(), Files.readAllBytes(path));
}
return found;
}
private static boolean equalFiles(Map<String, byte[]> a, Map<String, byte[]> b) {
return a.keySet().equals(b.keySet()) && a.entrySet().stream().allMatch(entry -> Arrays.equals(entry.getValue(), b.get(entry.getKey())));
}
private static void write(Path path, Map<String, Object> report) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(report)); }
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("failure", error.toString());
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
SanctuaryMod.LOGGER.error("Aerial24 fixture failed at {}", report.get("stage"), error);
try { write(Path.of("diagnostics/aerial24/failure.json"), report); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
}
}
@@ -61,12 +61,12 @@ public final class PopulationDiagnostics {
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) { if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES); var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES);
if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city"); if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city");
if (unified.rootGeneration() == 23) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
.forEach(name -> expected.add("minecraft:" + name)); .forEach(name -> expected.add("minecraft:" + name));
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName()) && generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
.collect(java.util.stream.Collectors.toSet()).equals(expected), .collect(java.util.stream.Collectors.toSet()).equals(expected),
"Historical holders stay intact; only generation23 adds the expansion ocean and shore palette"); "Historical holders stay intact; generations23/24 add 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 {
@@ -215,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() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures()); && (sanctuary.rootGeneration() == 24 || sanctuary.rootGeneration() == 23 || sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
// Generation19 explicitly prospects destinations across the root island before publishing them. // 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.
@@ -61,7 +61,10 @@ final class UnifiedWorldSettingsDiagnostics {
var historical22 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v22"))); var historical22 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v22")));
helper.assertTrue(historical22.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old helper.assertTrue(historical22.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 22 && old.source().generation() == 22, "Historical22 remains separately readable"); && 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"); var historical23 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v23")));
helper.assertTrue(historical23.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
&& old.rootGeneration() == 23 && old.source().generation() == 23, "Historical23 remains separately readable");
helper.assertTrue(generator.rootGeneration() == 24 && generator.source().generation() == 24 && generator.experimentalStructures(), "The public preset uses generation24 with its new-world option");
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)) {
@@ -72,11 +75,11 @@ final class UnifiedWorldSettingsDiagnostics {
} }
var ops = RegistryOps.create(JsonOps.INSTANCE, access); var ops = RegistryOps.create(JsonOps.INSTANCE, access);
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 activeCodec = generator.rootGeneration() == 24 ? SanctuaryChunkGenerator.AERIAL24_CODEC : generator.rootGeneration() == 23 ? SanctuaryChunkGenerator.SECRETS23_CODEC : generator.rootGeneration() == 22 ? SanctuaryChunkGenerator.SELECTIVE22_CODEC : generator.rootGeneration() == 21 ? SanctuaryChunkGenerator.NETWORK21_CODEC : generator.rootGeneration() == 20 ? SanctuaryChunkGenerator.ARCHITECTURE20_CODEC : generator.rootGeneration() == 19 ? SanctuaryChunkGenerator.HERITAGE19_CODEC : generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
var 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(),
"Generation23 omitted experimental option defaults to enabled"); "Generation24 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");
@@ -135,7 +138,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 && active.rootGeneration() != 23) return; || active.rootGeneration() != 21 && active.rootGeneration() != 22 && active.rootGeneration() != 23 && active.rootGeneration() != 24) 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),
@@ -145,7 +148,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 || active.rootGeneration() == 23) && helper.getLevel().structureManager().shouldGenerateStructures()) if ((active.rootGeneration() == 22 || active.rootGeneration() == 23 || active.rootGeneration() == 24) && helper.getLevel().structureManager().shouldGenerateStructures())
helper.assertTrue(!interior.isEmpty(), "The attached generation22 world has core construction to protect"); 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(),
@@ -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.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20") if (Boolean.getBoolean("sanctuary.test.aerial24.flowAdmission") || Boolean.getBoolean("sanctuary.test.aerial24") || Boolean.getBoolean("sanctuary.test.admission23") || Boolean.getBoolean("sanctuary.test.ocean23") || Boolean.getBoolean("sanctuary.test.secrets23") || Boolean.getBoolean("sanctuary.test.native23") || Boolean.getBoolean("sanctuary.test.selective22") || (Boolean.getBoolean("sanctuary.test.heritage20") || Boolean.getBoolean("sanctuary.test.heritage21") || Boolean.getBoolean("sanctuary.test.lostCity21") || Boolean.getBoolean("sanctuary.test.transit21")) || Boolean.getBoolean("sanctuary.test.lostCity20")
|| Boolean.getBoolean("sanctuary.test.heritage19") || Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity") || Boolean.getBoolean("sanctuary.test.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;
@@ -47,9 +47,9 @@ public abstract class SanctuaryGameTestServerMixin {
"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", 23); int generation = Integer.getInteger("sanctuary.test.generation", 24);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) 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 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 23 ? "sanctuary" : "sanctuary_v" + generation)); return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 24 ? "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);
@@ -76,6 +76,9 @@ public final class SanctuaryMod implements ModInitializer {
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.CHUNK_GENERATOR, id("island_v23"), SanctuaryChunkGenerator.SECRETS23_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC); Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v23"), SanctuaryBiomeSource.SECRETS23_CODEC);
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register(); 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();
@@ -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 || generator.rootGeneration() == 23) { if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
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() == 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) contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 24 ? "sanctuary.world_options.aerial24_hint" : generator.rootGeneration() == 23 ? "sanctuary.world_options.secrets23_hint" : generator.rootGeneration() == 22 ? "sanctuary.world_options.heritage22_hint" : (generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? "sanctuary.world_options.heritage_hint" : "sanctuary.world_options.experimental_hint"), font)
.setMaxWidth(310).setCentered(true)); .setMaxWidth(310).setCentered(true));
} }
layout.addToContents(contents); layout.addToContents(contents);
@@ -129,6 +129,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
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); fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
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) {
@@ -45,7 +45,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
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(generation).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)) if (relief.equals("ocean") && ((generation != 23 && generation != 24) || id.equals("sanctuary") || diameter != 512 && diameter != 1024))
throw new IllegalArgumentException("Ocean relief requires a generation23 expansion of diameter512 or1024"); 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");
@@ -59,7 +59,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
var priorities = new java.util.TreeSet<>(structurePriorities); var priorities = new java.util.TreeSet<>(structurePriorities);
if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family"); if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family");
structurePriorities = List.copyOf(priorities); structurePriorities = List.copyOf(priorities);
if (!structurePriorities.isEmpty() && (generation != 23 || id.equals("sanctuary"))) if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24) || id.equals("sanctuary")))
throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions"); throw new IllegalArgumentException("Structure priorities are only available for generation23 expansions");
} }
@@ -70,8 +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 || generation == 23; } public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24); }
public static List<String> reliefs(int generation) { return generation == 23 ? RELIEFS23 : RELIEFS; } public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24) ? 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(); }
@@ -10,6 +10,8 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.Strictness; import com.google.gson.Strictness;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24;
import fr.koka.sanctuary.worldgen.aerial.AerialStore24;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -41,6 +43,8 @@ public final class ExpansionJournal {
private final int rootDiameter; private final int rootDiameter;
private final int rootGeneration; private final int rootGeneration;
private final DirectorySync directorySync; private final DirectorySync directorySync;
private String aerialToken = "";
private AerialPlan24 aerialPlan24 = AerialPlan24.EMPTY;
private List<ExpansionIsland> islands; private List<ExpansionIsland> islands;
private boolean failed; private boolean failed;
@@ -52,7 +56,17 @@ public final class ExpansionJournal {
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory)); directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory));
} }
public ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, String initialAerialToken) throws IOException {
this(directory, seed, rootDiameter, rootGeneration,
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory), initialAerialToken);
}
ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync) throws IOException { ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync) throws IOException {
this(directory, seed, rootDiameter, rootGeneration, directorySync, "");
}
private ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync,
String initialAerialToken) throws IOException {
if (rootGeneration != 12 && rootGeneration != 13 && !ExpansionIsland.spatialGeneration(rootGeneration)) throw new IOException("Unsupported root generation"); if (rootGeneration != 12 && rootGeneration != 13 && !ExpansionIsland.spatialGeneration(rootGeneration)) throw new IOException("Unsupported root generation");
if (rootDiameter != 512 && rootDiameter != 724 && !((rootGeneration == 13 || ExpansionIsland.spatialGeneration(rootGeneration)) && rootDiameter == 1024)) if (rootDiameter != 512 && rootDiameter != 724 && !((rootGeneration == 13 || ExpansionIsland.spatialGeneration(rootGeneration)) && rootDiameter == 1024))
throw new IOException("Unsupported expansion root diameter"); throw new IOException("Unsupported expansion root diameter");
@@ -62,10 +76,13 @@ public final class ExpansionJournal {
this.file = this.directory.resolve(FILE_NAME); this.file = this.directory.resolve(FILE_NAME);
this.seed = seed; this.seed = seed;
this.rootDiameter = rootDiameter; this.rootDiameter = rootDiameter;
if (rootGeneration == 24 && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
else if (rootGeneration != 24 && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) { if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory"); if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory");
this.islands = read(); this.islands = read();
} else { } else {
if (rootGeneration == 24 && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
// Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair. // Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair.
Files.createDirectories(this.directory.getParent()); Files.createDirectories(this.directory.getParent());
Files.createDirectory(this.directory); Files.createDirectory(this.directory);
@@ -75,6 +92,8 @@ public final class ExpansionJournal {
} }
public synchronized List<ExpansionIsland> islands() { return islands; } public synchronized List<ExpansionIsland> islands() { return islands; }
public synchronized AerialPlan24 aerialPlan24() { return aerialPlan24; }
public synchronized String aerialToken() { return aerialToken; }
/** False after uncertain persistence or external damage; only a successful reload can recover. */ /** False after uncertain persistence or external damage; only a successful reload can recover. */
public synchronized boolean healthy() { return !failed; } public synchronized boolean healthy() { return !failed; }
@@ -150,17 +169,20 @@ public final class ExpansionJournal {
reader.setStrictness(Strictness.STRICT); reader.setStrictness(Strictness.STRICT);
JsonObject root = readJson(reader, 0).getAsJsonObject(); JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal"); if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal");
requireFields(root, JOURNAL_FIELDS); var journalFields = new java.util.HashSet<>(JOURNAL_FIELDS);
if (integer(root, "schema") != 1 || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) { if (rootGeneration == 24) journalFields.add("aerial_plan");
requireFields(root, journalFields);
if (integer(root, "schema") != (rootGeneration == 24 ? 2 : 1) || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
throw new IOException("Expansion journal schema, world seed or root diameter does not match"); throw new IOException("Expansion journal schema, world seed or root diameter does not match");
} }
if (rootGeneration == 24) bindAerial(string(root, "aerial_plan"));
JsonArray entries = root.getAsJsonArray("islands"); JsonArray entries = root.getAsJsonArray("islands");
List<ExpansionIsland> decoded = new ArrayList<>(); List<ExpansionIsland> decoded = new ArrayList<>();
for (JsonElement element : entries) { for (JsonElement element : entries) {
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"); if ((rootGeneration == 23 || rootGeneration == 24) && 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(),
@@ -191,6 +213,7 @@ public final class ExpansionJournal {
long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ(); long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ();
if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id()); if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id());
for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id()); for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id());
if (rootGeneration == 24 && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
previous.put(island.id(), island); previous.put(island.id(), island);
} }
return checked; return checked;
@@ -212,7 +235,8 @@ public final class ExpansionJournal {
private JsonObject encode(List<ExpansionIsland> entries) { private JsonObject encode(List<ExpansionIsland> entries) {
JsonObject root = new JsonObject(); JsonObject root = new JsonObject();
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter); root.addProperty("schema", rootGeneration == 24 ? 2 : 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", rootDiameter);
if (rootGeneration == 24) root.addProperty("aerial_plan", aerialToken);
JsonArray array = new JsonArray(); JsonArray array = new JsonArray();
for (ExpansionIsland island : entries) { for (ExpansionIsland island : entries) {
JsonObject entry = new JsonObject(); JsonObject entry = new JsonObject();
@@ -232,6 +256,14 @@ public final class ExpansionJournal {
return root; return root;
} }
private void bindAerial(String token) throws IOException {
if (!aerialToken.isEmpty() && !aerialToken.equals(token)) throw new IOException("Aerial manifest identity changed outside this journal");
var stored = AerialStore24.load(AerialStore24.directory(directory), token, seed, rootDiameter);
if (!aerialToken.isEmpty() && !aerialPlan24.sites().equals(stored.plan().sites())) throw new IOException("Aerial layout changed outside this journal");
aerialToken = token;
aerialPlan24 = stored.plan();
}
private static JsonElement readJson(JsonReader reader, int depth) throws IOException { private static JsonElement readJson(JsonReader reader, int depth) throws IOException {
if (depth > 4) throw new IOException("Expansion journal nesting exceeds its schema"); if (depth > 4) throw new IOException("Expansion journal nesting exceeds its schema");
return switch (reader.peek()) { return switch (reader.peek()) {
@@ -24,14 +24,14 @@ public final class ExpansionOcean23 {
} }
private ExpansionOcean23() {} private ExpansionOcean23() {}
public static boolean active(ExpansionIsland island) { public static boolean active(ExpansionIsland island) {
return island != null && island.generation() == 23 && !island.origin() && island.relief().equals("ocean"); return island != null && (island.generation() == 23 || island.generation() == 24) && !island.origin() && island.relief().equals("ocean");
} }
public static int seaLevel(ExpansionIsland island) { public static int seaLevel(ExpansionIsland island) {
return (int) Math.round(ExpansionRelief.worldY(island, 220)); return (int) Math.round(ExpansionRelief.worldY(island, 220));
} }
/** Quick expansions retain their size/climate draw; only unspecified relief participates. */ /** 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) { 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"; if ((generation != 23 && generation != 24) || diameter < 512 || climate.equals("lost_city")) return "natural";
return Long.remainderUnsigned(ExpansionIsland.deriveSeed(seed ^ 0x0CEA231L, id), 3) == 0 ? "ocean" : "natural"; return Long.remainderUnsigned(ExpansionIsland.deriveSeed(seed ^ 0x0CEA231L, id), 3) == 0 ? "ocean" : "natural";
} }
public static Column column(ExpansionIsland island, int x, int z) { public static Column column(ExpansionIsland island, int x, int z) {
@@ -1,6 +1,9 @@
package fr.koka.sanctuary.expansion; package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.SanctuaryMod; import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.aerial.AerialPlanner24;
import fr.koka.sanctuary.worldgen.aerial.AerialStore24;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
@@ -37,7 +40,19 @@ public final class ExpansionRuntime {
if (server.getWorldData().overworldData().isInitialized() if (server.getWorldData().overworldData().isInitialized()
&& !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) && !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS))
throw new IOException("Initialized laboratory world is missing its expansion journal directory"); throw new IOException("Initialized laboratory world is missing its expansion journal directory");
var journal = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration()); String initialAerialToken = "";
if (generator.rootGeneration() == 24 && !java.nio.file.Files.exists(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
initialAerialToken = AerialStore24.create(AerialStore24.directory(directory), level.getSeed(), generator.initialDiameter(),
() -> AerialPlanner24.forWorld(level, sanctuary)).token();
}
var journal = generator.rootGeneration() == 24
? new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), 24, initialAerialToken)
: new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
if (generator.rootGeneration() == 24) {
if (!(generator instanceof SanctuaryChunkGenerator sanctuary)) throw new IOException("Aerial world requires the Sanctuary generator");
sanctuary.setAerialPlan24(journal.aerialPlan24());
}
ExpansionVoidReuse.open(level, generator, journal.islands()); ExpansionVoidReuse.open(level, generator, journal.islands());
generator.initialize(level, journal.islands()); generator.initialize(level, journal.islands());
var session = new Session(level, generator, journal); var session = new Session(level, generator, journal);
@@ -87,7 +102,7 @@ public final class ExpansionRuntime {
public ExpansionIsland candidate(String id, String parentId, String direction, String climate, int diameter, String relief, public ExpansionIsland candidate(String id, String parentId, String direction, String climate, int diameter, String relief,
int minimumDistance) { int minimumDistance) {
return ExpansionPlacement.candidates(level.getSeed(), islands(), id, parentId, direction, climate, diameter, relief, minimumDistance) return ExpansionPlacement.candidates(level.getSeed(), islands(), id, parentId, direction, climate, diameter, relief, minimumDistance)
.stream().findFirst().orElseThrow(() -> new IllegalArgumentException("no_free_space")); .stream().filter(value -> !aerialConflicts(value)).findFirst().orElseThrow(() -> new IllegalArgumentException("no_free_space"));
} }
public ExpansionIsland create(String id, String parent, String direction, String climate, int diameter, String relief) throws IOException { public ExpansionIsland create(String id, String parent, String direction, String climate, int diameter, String relief) throws IOException {
return create(id, parent, direction, climate, diameter, relief, 0); return create(id, parent, direction, climate, diameter, relief, 0);
@@ -109,15 +124,15 @@ public final class ExpansionRuntime {
if (islands().size() >= 64) throw new IllegalArgumentException("island_limit"); if (islands().size() >= 64) throw new IllegalArgumentException("island_limit");
// Capture one immutable decision before any candidate is probed or certified. A resumed // 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. // reservation uses its recorded profile even if the world's census has since changed.
var priorities = generator.rootGeneration() == 23 var priorities = generator.rootGeneration() == 23 || generator.rootGeneration() == 24
? fr.koka.sanctuary.knowledge.structures.StructureCensusService.priorities(level) : List.<String>of(); ? 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) var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance)
.stream().map(value -> value.withStructurePriorities(priorities)).toList(); .stream().filter(value -> !aerialConflicts(value)).map(value -> value.withStructurePriorities(priorities)).toList();
boolean nativeRefused = false; 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); if (value.generation() == 23 || value.generation() == 24) 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");
@@ -143,6 +158,10 @@ public final class ExpansionRuntime {
ExpansionAdmission.checkDeadline(deadline); ExpansionAdmission.checkDeadline(deadline);
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space"); throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
} }
private boolean aerialConflicts(ExpansionIsland island) {
return generator.rootGeneration() == 24 && generator instanceof SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island);
}
public void resume(String id) { public void resume(String id) {
requireHealthyJournal(); requireHealthyJournal();
var island = island(id); var island = island(id);
@@ -44,7 +44,7 @@ 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) { if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level); ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
return; return;
} }
@@ -61,6 +61,7 @@ public final class ExpansionStructures23 {
public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); } public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); }
public static String family(String id) { return ExpansionStructureCatalog23.family(id); } public static String family(String id) { return ExpansionStructureCatalog23.family(id); }
public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; } public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; }
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24; }
public record Candidate(ChunkPos chunk, long roll) {} public record Candidate(ChunkPos chunk, long roll) {}
public record Accepted(Holder<Structure> structure, StructureStart start) {} public record Accepted(Holder<Structure> structure, StructureStart start) {}
public static final class NoSuitableStructureException extends IllegalArgumentException { public static final class NoSuitableStructureException extends IllegalArgumentException {
@@ -90,7 +91,7 @@ public final class ExpansionStructures23 {
StructureTemplateManager templates, ResourceKey<Level> dimension) { StructureTemplateManager templates, ResourceKey<Level> dimension) {
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return; if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ()); ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (island == null || island.generation() != 23) return; if (island == null || !nativeGeneration(island.generation())) return;
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null); Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
if (candidate == null) return; if (candidate == null) return;
for (var old : chunk.getAllStarts().values()) { for (var old : chunk.getAllStarts().values()) {
@@ -110,7 +111,7 @@ public final class ExpansionStructures23 {
* No active source snapshot is mutated and no real world chunk is requested. * No active source snapshot is mutated and no real world chunk is requested.
*/ */
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) { public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
if (island.generation() != 23 || island.origin() || !level.structureManager().shouldGenerateStructures()) return; if (!nativeGeneration(island.generation()) || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host)) if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host))
throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world"); throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world");
ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess()); ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess());
@@ -384,7 +385,7 @@ public final class ExpansionStructures23 {
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> wanted, public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> wanted,
BlockPos origin, int radius, boolean skipReferenced) { BlockPos origin, int radius, boolean skipReferenced) {
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| generator.rootGeneration() != 23 || !level.structureManager().shouldGenerateStructures()) return null; || !nativeGeneration(generator.rootGeneration()) || !level.structureManager().shouldGenerateStructures()) return null;
boolean nativeRequested = wanted.stream().anyMatch(h -> { boolean nativeRequested = wanted.stream().anyMatch(h -> {
var e = ExpansionStructureCatalog23.entry(h.unwrapKey().orElseThrow().identifier().toString()); var e = ExpansionStructureCatalog23.entry(h.unwrapKey().orElseThrow().identifier().toString());
return e != null && e.availability() == Availability.OVERWORLD; return e != null && e.availability() == Availability.OVERWORLD;
@@ -392,7 +393,7 @@ public final class ExpansionStructures23 {
boolean sanctuaryRequested = wanted.stream().anyMatch(h -> h.unwrapKey().orElseThrow().identifier().getNamespace().equals("sanctuary")); boolean sanctuaryRequested = wanted.stream().anyMatch(h -> h.unwrapKey().orElseThrow().identifier().getNamespace().equals("sanctuary"));
var candidates = new ArrayList<Candidate>(); var candidates = new ArrayList<Candidate>();
for (ExpansionIsland island : generator.source().islands()) { for (ExpansionIsland island : generator.source().islands()) {
if (!nativeRequested || island.generation() != 23 || !island.status().equals("ready")) continue; if (!nativeRequested || !nativeGeneration(island.generation()) || !island.status().equals("ready")) continue;
for (Candidate candidate : candidates(island)) { for (Candidate candidate : candidates(island)) {
if (Math.abs((long) candidate.chunk().x() - (origin.getX() >> 4)) <= (long) radius * CELL_CHUNKS 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); && Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
@@ -429,7 +430,7 @@ public final class ExpansionStructures23 {
private static void addSanctuaryCandidates(SanctuaryChunkGenerator generator, List<Candidate> positions) { private static void addSanctuaryCandidates(SanctuaryChunkGenerator generator, List<Candidate> positions) {
for (ExpansionIsland island : generator.source().islands()) { for (ExpansionIsland island : generator.source().islands()) {
if (island.generation() != 23 || !island.status().equals("ready")) continue; if (!nativeGeneration(island.generation()) || !island.status().equals("ready")) continue;
for (var district : generator.lostCityPlan(island).districts()) addPosition(positions, district.startChunkX(), district.startChunkZ()); 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().sites().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
@@ -439,6 +440,8 @@ public final class ExpansionStructures23 {
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ())); generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
var mineral = generator.mineralSanctuary23(); var mineral = generator.mineralSanctuary23();
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ()); if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
.forEach(site -> addPosition(positions, site.startChunkX(), site.startChunkZ()));
} }
private static void addPosition(List<Candidate> positions, int x, int z) { positions.add(new Candidate(new ChunkPos(x, z), 0)); } 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) { private static BlockPos locator(StructureStart start, BlockPos origin) {
@@ -536,7 +539,7 @@ public final class ExpansionStructures23 {
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) { NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries; super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
} }
@Override public int rootGeneration() { return 23; } @Override public int rootGeneration() { return island.generation(); }
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); } @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 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 RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
@@ -86,6 +86,9 @@ public final class ExpansionVoidReuse {
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin()) || !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin())
throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion"); throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion");
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
&& sanctuary.aerialPlan24().conflicts(island))
throw new ExpansionAdmission.OccupiedException("aerial_reserved: expansion overlaps a permanent initial site");
List<ChunkPos> positions = positions(island); List<ChunkPos> positions = positions(island);
var entityStorage = entityRegion(level); var entityStorage = entityRegion(level);
var poiStorage = ((ExpansionVoidPoiStorageAccess) level.getPoiManager()).sanctuary$simpleRegionStorage(); var poiStorage = ((ExpansionVoidPoiStorageAccess) level.getPoiManager()).sanctuary$simpleRegionStorage();
@@ -173,6 +176,9 @@ public final class ExpansionVoidReuse {
public static void open(ServerLevel level, ExpansionChunkGenerator generator, List<ExpansionIsland> islands) throws IOException { public static void open(ServerLevel level, ExpansionChunkGenerator generator, List<ExpansionIsland> islands) throws IOException {
requireServerThread(level); close(level); requireServerThread(level); close(level);
if (!ExpansionIsland.spatialGeneration(generator.rootGeneration())) return; if (!ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary)
for (var island : islands) if (sanctuary.aerialPlan24().conflicts(island))
throw new IOException("Saved expansion overlaps a permanent aerial site: " + island.id());
var state = new State(); var state = new State();
for (var island : islands) if (!island.reuseToken().isEmpty()) add(state, loadManifest(level, island)); for (var island : islands) if (!island.reuseToken().isEmpty()) add(state, loadManifest(level, island));
// Validate every transaction before writing even an EMPTY placeholder or publishing any island. // Validate every transaction before writing even an EMPTY placeholder or publishing any island.
@@ -16,6 +16,12 @@ public final class StructureCensusCommands {
public static void register() { public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) -> { CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) -> {
var structures = Commands.literal("structures").executes(context -> report(context.getSource(), null)) var structures = Commands.literal("structures").executes(context -> report(context.getSource(), null))
.then(Commands.literal("aerial").executes(context -> aerial(context.getSource(), null))
.then(Commands.argument("site", StringArgumentType.word())
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
StructureCensusService.aerialSites(context.getSource().getLevel()).stream()
.map(StructureCensusService.AerialSite::id), builder))
.executes(context -> aerial(context.getSource(), StringArgumentType.getString(context, "site")))))
.then(Commands.literal("island").then(Commands.argument("id", StringArgumentType.word()) .then(Commands.literal("island").then(Commands.argument("id", StringArgumentType.word())
.suggests((context, builder) -> SharedSuggestionProvider.suggest( .suggests((context, builder) -> SharedSuggestionProvider.suggest(
StructureCensusService.snapshot(context.getSource().getServer()).regions().stream() StructureCensusService.snapshot(context.getSource().getServer()).regions().stream()
@@ -51,6 +57,23 @@ public final class StructureCensusCommands {
send(source, "help"); send(source, "help");
return 1; return 1;
} }
private static int aerial(CommandSourceStack source, String selected) {
var sites = StructureCensusService.aerialSites(source.getLevel()).stream()
.filter(site -> selected == null || site.id().equals(selected)).toList();
if (sites.isEmpty()) {
source.sendFailure(selected == null ? Component.translatable(TEXT + "aerial.none")
: Component.translatable(TEXT + "aerial.unknown_site", selected));
return 0;
}
for (var site : sites) {
var coverage = site.coverage();
send(source, "aerial.site", site.id(), site.structure(),
Component.translatable(TEXT + "aerial.status." + site.status().name().toLowerCase(java.util.Locale.ROOT)),
coverage.startsChunks(), coverage.expectedChunks(), coverage.featurePassChunks(), coverage.expectedChunks(),
coverage.fullChunks(), coverage.expectedChunks());
}
return 1;
}
private static int dimension(CommandSourceStack source, String id) { private static int dimension(CommandSourceStack source, String id) {
var snapshot = StructureCensusService.snapshot(source.getServer()); var snapshot = StructureCensusService.snapshot(source.getServer());
if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; } if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; }
@@ -83,7 +83,8 @@ public final class StructureCensusService {
public static List<String> priorities(ServerLevel level) { public static List<String> priorities(ServerLevel level) {
assertServerThread(level.getServer()); assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures() if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) || generator.rootGeneration() != 23) || !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24)
return List.of(); return List.of();
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString()); return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
} }
@@ -156,7 +157,7 @@ public final class StructureCensusService {
}).toList(); }).toList();
if (matching.isEmpty()) return Presence.OTHER_DIMENSION; if (matching.isEmpty()) return Presence.OTHER_DIMENSION;
// This is an explicit generation23 habitat requirement, not an inferred lack of biomes. // This is an explicit generation23 habitat requirement, not an inferred lack of biomes.
if (island != null && island.generation() == 23 && !island.relief().equals("ocean") if (island != null && (island.generation() == 23 || island.generation() == 24) && !island.relief().equals("ocean")
&& matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE; && matching.stream().allMatch(entry -> entry.habitat() == ExpansionStructureCatalog23.Habitat.OCEAN)) return Presence.INCOMPATIBLE;
return null; return null;
} }
@@ -172,6 +173,31 @@ public final class StructureCensusService {
.map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id); .map(ExpansionStructureCatalog23.Entry::family).findFirst().orElse(id);
} }
/** Planned aerial sites have their own finite scopes. Planning is never recorded as
* generated evidence, and these sites never enter the vanilla absence-priority pool. */
public static List<AerialSite> aerialSites(ServerLevel level) {
assertServerThread(level.getServer());
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|| !(level.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator generator)
|| generator.rootGeneration() != 24) return List.of();
var session = current(level.getServer());
var evidence = session.memory.snapshot();
String dimension = level.dimension().identifier().toString();
var result = new ArrayList<AerialSite>();
for (var site : generator.aerialPlan24().sites()) {
String structure = fr.koka.sanctuary.worldgen.aerial.AerialStructures24.structureId(site);
var scope = site.claimedChunks().stream().map(chunk -> new ChunkKey(dimension, chunk.x(), chunk.z())).toList();
var coverage = evidence.coverage(scope);
var key = new SiteKey(dimension, structure, site.startChunkX(), 0, site.startChunkZ(), false);
var observed = evidence.sites().get(key);
AerialStatus status = session.failed ? AerialStatus.UNKNOWN : observed != null
? evidence.generated(observed) ? AerialStatus.GENERATED : AerialStatus.ACCEPTED
: coverage.startsComplete() ? AerialStatus.MISSING : AerialStatus.PLANNED;
result.add(new AerialSite(site.id(), structure, dimension, coverage, status));
}
return List.copyOf(result);
}
/** Finite ownership is derived from the immutable expansion journal descriptors, /** Finite ownership is derived from the immutable expansion journal descriptors,
* rather than duplicated in the sidecar or inferred from currently loaded chunks. */ * rather than duplicated in the sidecar or inferred from currently loaded chunks. */
public static List<ChunkKey> scope(String dimension, ExpansionIsland island) { public static List<ChunkKey> scope(String dimension, ExpansionIsland island) {
@@ -222,6 +248,8 @@ public final class StructureCensusService {
Session(StructureCensusStore store) { this.store = store; } Session(StructureCensusStore store) { this.store = store; }
} }
public enum Presence { PRESENT, ABSENT, UNKNOWN, INCOMPATIBLE, OTHER_DIMENSION } public enum Presence { PRESENT, ABSENT, UNKNOWN, INCOMPATIBLE, OTHER_DIMENSION }
public enum AerialStatus { PLANNED, ACCEPTED, GENERATED, MISSING, UNKNOWN }
public record AerialSite(String id, String structure, String dimension, Coverage coverage, AerialStatus status) {}
public record FamilyCount(int accepted, int generated, Presence presence) {} 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 record Region(String dimension, String island, int generation, String status, Coverage coverage, Map<String, FamilyCount> families) {
public Region { families = Map.copyOf(families); } public Region { families = Map.copyOf(families); }
@@ -20,6 +20,11 @@ public abstract class UnifiedDecorationProtectionMixin {
var region = (WorldGenRegion) (Object) this; var region = (WorldGenRegion) (Object) this;
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator) if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return; || !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return; // Vanilla still applies its write-zone and build-height checks.
}
var center = region.getCenter(); var center = region.getCenter();
var owner = generator.islandForChunk(center.x(), center.z()); var owner = generator.islandForChunk(center.x(), center.z());
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4); var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
@@ -39,7 +44,11 @@ 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 (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return; if (fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.isPlacing()) return;
var island = generator.islandAt(pos.getX(), pos.getZ()); var island = generator.islandAt(pos.getX(), pos.getZ());
if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island) if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island)
@@ -55,7 +64,7 @@ public abstract class UnifiedDecorationProtectionMixin {
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 || generator.rootGeneration() == 23) if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& 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)) {
@@ -62,6 +62,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, 23))); ).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 23)));
public static final MapCodec<SanctuaryBiomeSource> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 24)));
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;
@@ -77,7 +81,7 @@ 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) if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME, required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
net.minecraft.resources.Identifier.withDefaultNamespace(name))); 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,
@@ -98,7 +102,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 == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; } @Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
@Override public BiomeResolver createResolver(Climate.Sampler sampler) { @Override public BiomeResolver createResolver(Climate.Sampler sampler) {
BiomeResolver woodlands = population.createResolver(sampler); BiomeResolver woodlands = population.createResolver(sampler);
@@ -75,6 +75,41 @@ 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, 23, experimental))); ).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 23, experimental)));
public static final MapCodec<SanctuaryChunkGenerator> AERIAL24_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
SanctuaryBiomeSource.AERIAL24_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, 24, experimental)));
private volatile fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24 = fr.koka.sanctuary.worldgen.aerial.AerialPlan24.EMPTY;
public fr.koka.sanctuary.worldgen.aerial.AerialPlan24 aerialPlan24() { return aerialPlan24; }
public void setAerialPlan24(fr.koka.sanctuary.worldgen.aerial.AerialPlan24 plan) {
if (generation != 24) throw new IllegalStateException("Aerial plans require generation24");
aerialPlan24 = java.util.Objects.requireNonNull(plan);
}
/** Plan root secrets before certifying aerial shore viewpoints, without requesting chunks. */
public void prepareAerialSampling24(ServerLevel level) {
if (generation != 24) throw new IllegalStateException("Aerial sampling requires generation24");
heightAccessor = level;
cityStructuresEnabled = level.structureManager().shouldGenerateStructures();
var root = java.util.List.of(ExpansionIsland.origin(initialDiameter(), level.getSeed(), 24));
super.initialize(level, root);
context(root.getFirst());
// Same order as initialize. Prepared plans are reused after the manifest is persisted.
prepareCities(root);
prepareTransit(root);
prepareUndergroundHall22(root);
prepareMineral23(root);
prepareRuins(root);
prepareHeritage(root);
prepareTransit(root);
}
public net.minecraft.world.level.NoiseColumn aerialBaseColumn24(int x, int z) {
var current = originContext;
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
}
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY; private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
private boolean mineralPrepared23; private boolean mineralPrepared23;
public fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23() { return mineralSanctuary23; } public fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23() { return mineralSanctuary23; }
@@ -130,12 +165,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 || generation == 23) && experimental; this.experimentalStructures = (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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 == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; } @Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) { public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures); return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
} }
@@ -146,11 +181,15 @@ 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 == 23 && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ) if (generation == 24) for (var site : aerialPlan24.sites()) {
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ) if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ) && minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
|| (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); return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
} }
public IslandCapacity capacity() { public IslandCapacity capacity() {
return switch (initialDiameter()) { return switch (initialDiameter()) {
@@ -185,7 +224,7 @@ 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 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); } if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands); prepareMineral23(islands);
prepareRuins(islands); prepareRuins(islands);
prepareHeritage(islands); prepareHeritage(islands);
@@ -194,7 +233,7 @@ 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 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); } if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
prepareMineral23(islands); prepareMineral23(islands);
prepareRuins(islands); prepareRuins(islands);
prepareHeritage(islands); prepareHeritage(islands);
@@ -209,7 +248,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 || generation == 23) && !cityStructuresEnabled) { if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && !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());
@@ -225,7 +264,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 || generation == 23) { if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
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));
@@ -240,13 +279,13 @@ 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 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return; if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !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 = generation == 23 var plan = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.ruins.RuinsPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled) ? 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); : fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
ruinsPlan17 = plan; ruinsPlan17 = plan;
@@ -255,16 +294,16 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
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 && generation != 23) || !experimentalStructures || !cityStructuresEnabled || heritagePrepared) return; if ((generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || !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 || generation == 23) { if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
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 == 23 var plan = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled) ? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation == 22 : 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)
@@ -289,7 +328,7 @@ 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 != 23) || generation == 21 && !experimentalStructures || !cityStructuresEnabled || transitPrepared) return; if ((generation != 21 && generation != 22 && (generation != 23 && generation != 24)) || 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(),
@@ -300,7 +339,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled) ? fr.koka.sanctuary.worldgen.transit.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 || generation == 23) && transitPlan21.isEmpty()) { if ((generation == 22 || (generation == 23 || generation == 24)) && 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()));
@@ -311,12 +350,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 || generation == 23) && (transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ()) return (generation == 22 || (generation == 23 || generation == 24)) && (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())); || (generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ()));
} }
private void prepareUndergroundHall22(List<ExpansionIsland> islands) { private void prepareUndergroundHall22(List<ExpansionIsland> islands) {
if ((generation != 22 && generation != 23) || !cityStructuresEnabled || undergroundHallPrepared22) return; if ((generation != 22 && (generation != 23 && generation != 24)) || !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)) {
@@ -333,7 +372,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
@Override public boolean protectedAt(int x, int y, int z) { return sampled.protectedAt(x, y, z); } @Override public boolean 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 undergroundHall22 = (generation == 23 || generation == 24)
? fr.koka.sanctuary.worldgen.city.LostCityUnderground23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain) ? 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); : 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();
@@ -343,7 +382,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
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) { private void prepareMineral23(List<ExpansionIsland> islands) {
if (generation != 23 || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) return; if ((generation != 23 && generation != 24) || !experimentalStructures || !cityStructuresEnabled || mineralPrepared23) 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.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor, var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
@@ -381,13 +420,13 @@ 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 ((generation == 23 || generation == 24) && 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 || generation == 23) { if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
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
@@ -492,12 +531,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}); });
} }
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) { public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true; if (generation == 24 && aerialPlan24.protects(pos)) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()) if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()) || (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ()) || (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || generation == 23) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ()); || (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
if (generation == 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());
@@ -519,20 +559,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 || generation == 23) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ); return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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 && generation != 23) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0); if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) 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 && generation != 23) return List.of(); if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) 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 && generation != 23) return null; if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return null;
lostCityPlan(island); lostCityPlan(island);
return cityReservations16.get(cityIdentity(island)); return cityReservations16.get(cityIdentity(island));
} }
@@ -542,16 +582,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 == 23 || generation == 24)) 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 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY)) if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.heritage.HeritageStructures19.KEY)) if ((generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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 || generation == 23) && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.transit.TransitStructures21.KEY)) if ((generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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);
} }
@@ -580,7 +620,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 || generation == 23) && cityStructuresEnabled) { if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && 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();
@@ -0,0 +1,139 @@
package fr.koka.sanctuary.worldgen.aerial;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.BlockPos;
/** Shared geometry for rendering, native locators and executable interior witnesses. */
public final class AerialGeometry24 {
private AerialGeometry24() {}
public static BlockPos world(AerialPlan24.Site site, int x, int y, int z) {
return new BlockPos(site.worldX(x, z), site.minY() + y, site.worldZ(x, z));
}
public static BlockPos chest(AerialPlan24.Site s) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2 - 2, 2, s.depth() / 2 + 2);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22);
case MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z);
}
};
}
public static BlockPos occupant(AerialPlan24.Site s) {
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants");
return world(s, 5, 6, 20);
}
public static UUID occupantUuid(AerialPlan24.Site s) {
return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant")
.getBytes(StandardCharsets.UTF_8));
}
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 7, 6, 19); }
public static BlockPos waterfallSource(AerialPlan24.Site s) {
if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall");
return world(s, s.width() / 2, rockHeight(s) - 3, 3);
}
public static BlockPos entry(AerialPlan24.Site s) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12);
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
};
}
public static List<BlockPos> interiorChecks(AerialPlan24.Site s) {
if (s.merchant()) return List.of(entry(s), world(s, 5, 6, 16), occupant(s), world(s, 5, 6, 23));
if (s.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2));
return List.of(entry(s));
}
public static int terrainTop(AerialPlan24.Site s, int x, int z) {
int height = rockHeight(s);
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 2 && z <= 6) return height - 4;
if (radial(s, x, z) < .13) return height - 1;
double phase = (s.seed() & 1023) * .01;
return Math.clamp(height - 3 + (int) Math.round(2 * Math.sin(x * .17 + phase) + 1.5 * Math.cos(z * .13 - phase)), height - 7, height - 1);
}
public static int rockHeight(AerialPlan24.Site s) { return s.rockHeight(); }
public static double radial(AerialPlan24.Site s, int x, int z) {
double dx = (x - (s.width() - 1) * .5) / (s.width() * .48);
double dz = (z - (s.depth() - 1) * .5) / (s.depth() * .48);
double phase = (s.seed() & 1023) * .01, angle = Math.atan2(dz, dx);
double edge = .84 + .105 * Math.sin(angle * 3 + phase) + .055 * Math.cos(angle * 5 - phase)
+ .025 * Math.sin(angle * 9 + phase * .7);
if (s.waterfall()) edge = Math.max(edge, .9 - 2.5 * Math.pow(angle + Math.PI / 2, 2));
return Math.hypot(dx, dz) / edge;
}
public static int terrainBottom(AerialPlan24.Site s, int x, int z) {
double r = Math.min(1, radial(s, x, z));
if (r < .065) return 0;
int top = terrainTop(s, x, z);
double taper = (r - .065) / .935;
double angle = Math.atan2(z - s.depth() * .5, x - s.width() * .5);
double ribs = 2 * Math.sin(angle * 5 + (s.seed() & 255) * .02) * taper * (1 - taper);
// The underside meets the rim within two blocks, instead of ending in a vertical cylinder.
return Math.clamp((int) Math.round((top - 1) * Math.pow(taper, .85) + ribs), 0, top - 1);
}
/** Open rock fractures deepen towards the rim; the central arrival and buried cache stay intact. */
public static int fractureFloor(AerialPlan24.Site s, int x, int z) {
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 3 && z <= 8) return Integer.MAX_VALUE;
double dx = x - (s.width() - 1) * .5, dz = z - (s.depth() - 1) * .5;
double phase = (s.seed() & 1023) * .006, start = Math.min(s.width(), s.depth()) * .21;
int floor = Integer.MAX_VALUE;
for (int fracture = 0; fracture < 2; fracture++) {
double angle = phase + fracture * 2.45;
double along = dx * Math.cos(angle) + dz * Math.sin(angle);
double across = -dx * Math.sin(angle) + dz * Math.cos(angle);
double bend = 1.1 * Math.sin(along * .24 + fracture);
if (along > start && Math.abs(across + bend) < 1.05 + (along - start) * .09)
floor = Math.min(floor, terrainTop(s, x, z) - 2 - (int) ((along - start) * 1.3));
}
return floor;
}
public static boolean mineralBody(AerialPlan24.Site s, int x, int y, int z) {
if (x < 0 || z < 0 || x >= s.width() || z >= s.depth()) return false;
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 2 && z <= 6)
return y >= rockHeight(s) - 8 + Math.max(0, 3 - z) && y <= rockHeight(s) - 4;
return radial(s, x, z) <= 1 && y >= terrainBottom(s, x, z) && y <= terrainTop(s, x, z)
&& y < fractureFloor(s, x, z);
}
/** Continuous soil cover broken by rocky edges, exposed seams and open fractures. */
public static boolean naturalSurface(AerialPlan24.Site s, int x, int z) {
if (s.waterfall() && Math.abs(x - s.width() / 2) <= 4 && z <= 9) return false;
int top = terrainTop(s, x, z);
if (radial(s, x, z) > .97 || !mineralBody(s, x, top, z) || !mineralBody(s, x, top - 1, z)) return false;
double phase = (s.seed() & 1023) * .01;
double patches = .6 * Math.sin(x * .17 + phase) + .45 * Math.cos(z * .14 - phase) + .25 * Math.sin((x + z) * .24);
return patches > -.75;
}
public static boolean arrivalColumn(AerialPlan24.Site s, int x, int z) {
return Math.abs(x - s.width() / 2) <= 1 && Math.abs(z - s.depth() / 2) <= 1;
}
public record Tree(int x, int groundY, int z, int trunkHeight, boolean birch) {}
/** Small woodland groups are planned against the whole site before any chunk is rendered. */
public static List<Tree> trees(AerialPlan24.Site s) {
if (s.kind() != AerialPlan24.Kind.MINERAL_ISLET) return List.of();
var trees = new ArrayList<Tree>();
int target = 2 + (s.width() - 40) / 10;
for (int attempt = 0; attempt < 256 && trees.size() < target; attempt++) {
long hash = mix(s.seed() ^ 0x24A11F0AE57L ^ (attempt * 0x9e3779b97f4a7c15L));
int x = 6 + (int) Math.floorMod(hash, s.width() - 12);
int z = 6 + (int) Math.floorMod(mix(hash), s.depth() - 12);
int dx = x - s.width() / 2, dz = z - s.depth() / 2;
if (dx * dx + dz * dz < 36 || (dx - 4) * (dx - 4) + (dz - 2) * (dz - 2) < 25) continue;
if (trees.stream().anyMatch(t -> (t.x() - x) * (t.x() - x) + (t.z() - z) * (t.z() - z) < 81)) continue;
int ground = terrainTop(s, x, z);
if (!naturalSurface(s, x, z)) continue;
boolean supported = true;
for (int ox = -2; ox <= 2 && supported; ox++) for (int oz = -2; oz <= 2; oz++) {
int neighbour = terrainTop(s, x + ox, z + oz);
if (Math.abs(neighbour - ground) > 1 || !mineralBody(s, x + ox, neighbour, z + oz)) {
supported = false; break;
}
}
if (supported) trees.add(new Tree(x, ground, z, 4 + (int) (hash & 1), trees.size() % 3 == 1));
}
return List.copyOf(trees);
}
static long mix(long n) { n = (n ^ n >>> 30) * 0xbf58476d1ce4e5b9L; n = (n ^ n >>> 27) * 0x94d049bb133111ebL; return n ^ n >>> 31; }
}
@@ -0,0 +1,113 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.ByteTag;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.LongTag;
import net.minecraft.nbt.StringTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.npc.villager.VillagerProfession;
import net.minecraft.world.entity.npc.villager.VillagerType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.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;
/** Native geometry and one-shot occupant flag; neither removed loot nor an extracted merchant is respawned. */
public final class AerialPiece24 extends StructurePiece {
private final AerialPlan24.Site site;
private boolean occupantPlaced;
public AerialPiece24(AerialPlan24.Site site) {
super(AerialStructures24.PIECE, 0, site.bounds()); this.site = site; setOrientation(null);
}
public AerialPiece24(StructurePieceSerializationContext context, CompoundTag tag) {
super(AerialStructures24.PIECE, tag);
site = decode(tag.getCompound("SanctuaryAerial").orElseThrow());
if (!(tag.get("OccupantPlaced") instanceof ByteTag)) throw new IllegalArgumentException("Missing occupant marker");
byte value = tag.getByte("OccupantPlaced").orElseThrow();
if (value != 0 && value != 1) throw new IllegalArgumentException("Invalid occupant marker");
occupantPlaced = value == 1;
if (occupantPlaced && !site.merchant()) throw new IllegalArgumentException("Non-merchant site has occupant marker");
if (!getBoundingBox().equals(site.bounds())) throw new IllegalArgumentException("Aerial piece bounding box differs from saved geometry");
setOrientation(null);
}
public AerialPlan24.Site site() { return site; }
public boolean occupantPlaced() { return occupantPlaced; }
public static CompoundTag encode(AerialPlan24.Site s) {
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Id", s.id()); tag.putString("Kind", s.kind().name());
tag.putInt("X", s.minX()); tag.putInt("Y", s.minY()); tag.putInt("Z", s.minZ());
tag.putInt("Width", s.width()); tag.putInt("Height", s.height()); tag.putInt("Depth", s.depth());
tag.putInt("Facing", s.facing()); tag.putLong("Seed", s.seed()); tag.putInt("ShoreX", s.shoreX());
tag.putInt("ShoreY", s.shoreY()); tag.putInt("ShoreZ", s.shoreZ()); tag.putBoolean("Waterfall", s.waterfall()); return tag;
}
public static AerialPlan24.Site decode(CompoundTag tag) {
if (!tag.keySet().equals(Set.of("Schema", "Id", "Kind", "X", "Y", "Z", "Width", "Height", "Depth", "Facing", "Seed", "ShoreX", "ShoreY", "ShoreZ", "Waterfall")))
throw new IllegalArgumentException("Unexpected aerial piece fields");
for (String name : new String[]{"Schema", "X", "Y", "Z", "Width", "Height", "Depth", "Facing", "ShoreX", "ShoreY", "ShoreZ"})
if (!(tag.get(name) instanceof IntTag)) throw new IllegalArgumentException("Expected aerial integer " + name);
if (!(tag.get("Id") instanceof StringTag) || !(tag.get("Kind") instanceof StringTag) || !(tag.get("Seed") instanceof LongTag)
|| !(tag.get("Waterfall") instanceof ByteTag) || tag.getInt("Schema").orElseThrow() != 1)
throw new IllegalArgumentException("Invalid aerial schema or fields");
byte waterfall = tag.getByte("Waterfall").orElseThrow();
if (waterfall != 0 && waterfall != 1) throw new IllegalArgumentException("Invalid waterfall flag");
return new AerialPlan24.Site(tag.getString("Id").orElseThrow(), AerialPlan24.Kind.valueOf(tag.getString("Kind").orElseThrow()),
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(), tag.getInt("ShoreX").orElseThrow(),
tag.getInt("ShoreY").orElseThrow(), tag.getInt("ShoreZ").orElseThrow(), waterfall == 1);
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.put("SanctuaryAerial", encode(site)); tag.putBoolean("OccupantPlaced", occupantPlaced);
}
@Override public BlockPos getLocatorPosition() { return AerialGeometry24.entry(site); }
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
AerialStructures24.placing(site, () -> {
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
private BlockPos position(int x, int y, int z) { return new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z); }
@Override public void set(int x, int y, int z, BlockState state) {
var pos = position(x, y, z);
if (site.bounds().isInside(pos) && clip.isInside(pos)) {
placeBlock(level, state, pos.getX(), pos.getY(), pos.getZ(), clip);
if (state.is(Blocks.WATER)) level.getChunk(pos).markPosForPostProcessing(pos);
}
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
var pos = position(x, y, z);
if (site.bounds().isInside(pos) && clip.isInside(pos)) createChest(level, clip, RandomSource.create(seed), pos, table, null);
}
}, clip);
placeOccupant(level, clip);
});
}
private synchronized void placeOccupant(WorldGenLevel level, BoundingBox clip) {
if (!site.merchant() || occupantPlaced) return;
var at = AerialGeometry24.occupant(site);
if (!clip.isInside(at)) return;
// Match vanilla structure-owned occupants: the persisted flag records placement, not continued presence.
var villager = EntityTypes.VILLAGER.create(level.getLevel(), EntitySpawnReason.STRUCTURE);
if (villager == null) throw new IllegalStateException("Merchant entity could not be created");
villager.snapTo(at.getX() + .5, at.getY(), at.getZ() + .5, site.facing() * 90, 0);
villager.finalizeSpawn(level, level.getCurrentDifficultyAt(at), EntitySpawnReason.STRUCTURE, null);
var profession = site.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? VillagerProfession.FARMER : VillagerProfession.TOOLSMITH;
villager.setVillagerData(villager.getVillagerData().withType(level.registryAccess(), VillagerType.PLAINS)
.withProfession(level.registryAccess(), profession).withLevel(1));
villager.setVillagerDataFinalized(true);
villager.setVillagerXp(1); // A previously practising merchant keeps the profession when carried away from its workstation.
villager.setPersistenceRequired(); villager.setUUID(AerialGeometry24.occupantUuid(site));
occupantPlaced = true;
level.addFreshEntityWithPassengers(villager);
}
}
@@ -0,0 +1,129 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
/** Immutable initial sites. Claims reserve only their columns, never new terrain or a surrounding ring. */
public final class AerialPlan24 {
/** Vegetation occupies additional height; it never replaces the twenty to thirty-five blocks of rock. */
public static final int VEGETATION_HEADROOM = 8;
/** Overworld water can spread seven blocks horizontally after landing. Only the outlet reserves this neighbourhood. */
public static final int WATERFALL_SPREAD = 7;
public static final AerialPlan24 EMPTY = new AerialPlan24(List.of());
private final List<Site> sites;
private final Set<Long> claims;
public enum Kind { BALLOON, MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
public AerialPlan24(List<Site> sites) {
this.sites = List.copyOf(sites);
if (!sites.isEmpty() && (sites.size() != 5
|| sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|| sites.stream().filter(Site::waterfall).count() != 1))
throw new IllegalArgumentException("An aerial plan contains all five sites and exactly one waterfall, or is disabled");
var ids = new HashSet<String>();
var columns = new HashSet<Long>();
for (Site site : sites) {
if (!ids.add(site.id())) throw new IllegalArgumentException("Duplicate aerial site");
for (ChunkPos chunk : site.claimedChunks()) if (!columns.add(chunk.pack()))
throw new IllegalArgumentException("Overlapping aerial claims");
}
if (columns.size() > 512) throw new IllegalArgumentException("Aerial claims exceed their finite budget");
claims = Set.copyOf(columns);
}
public List<Site> sites() { return sites; }
public boolean isEmpty() { return sites.isEmpty(); }
public Set<Long> claimsChunks() { return claims; }
public List<Site> atStartChunk(int x, int z) {
return sites.stream().filter(s -> s.startChunkX() == x && s.startChunkZ() == z).toList();
}
public boolean ownsChunk(int x, int z) { return claims.contains(ChunkPos.pack(x, z)); }
public boolean conflicts(ExpansionIsland island) {
if (island.origin()) return false;
for (long packed : claims) {
var chunk = ChunkPos.unpack(packed);
if (island.ownsChunk(chunk.x(), chunk.z())) return true;
}
return false;
}
public boolean protects(BlockPos pos) { return sites.stream().anyMatch(s -> s.mayWrite(pos)); }
/** Width and depth are local dimensions; facing rotates them around the world-space bounding-box minimum. */
public record Site(String id, Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
int facing, long seed, int shoreX, int shoreY, int shoreZ, boolean waterfall) {
public Site {
if (id == null || !id.matches("[a-z][a-z0-9_]{0,47}") || kind == null || facing < 0 || facing > 3
|| Math.abs((long) minX) > 2048 || Math.abs((long) minZ) > 2048
|| Math.abs((long) shoreX) > 1024 || Math.abs((long) shoreZ) > 1024
|| shoreY < 1 || shoreY > 383 || minY < 32 || height < 8 || height > 35 + VEGETATION_HEADROOM || minY + height > 350
|| width < 7 || width > 64 || depth < 7 || depth > 64
|| waterfall && kind != Kind.MINERAL_ISLET)
throw new IllegalArgumentException("Invalid aerial site geometry");
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
|| kind == Kind.BALLOON && height != 28
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18))
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
int worldWidth = facing % 2 == 0 ? width : depth;
int worldDepth = facing % 2 == 0 ? depth : width;
double cx = minX + (worldWidth - 1) / 2.0, cz = minZ + (worldDepth - 1) / 2.0;
if (Math.hypot(cx - shoreX, cz - shoreZ) > 512)
throw new IllegalArgumentException("Aerial site is farther than512 blocks from its shore witness");
}
public int worldWidth() { return facing % 2 == 0 ? width : depth; }
public int worldDepth() { return facing % 2 == 0 ? depth : width; }
public int rockHeight() {
if (kind != Kind.MINERAL_ISLET) throw new IllegalStateException("Only mineral islets have a rock height");
return height - VEGETATION_HEADROOM;
}
public int maxX() { return minX + worldWidth() - 1; }
public int maxY() { return minY + height - 1; }
public int maxZ() { return minZ + worldDepth() - 1; }
public int worldX(int x, int z) {
return minX + switch (facing) { case 0 -> x; case 1 -> depth - 1 - z; case 2 -> width - 1 - x; default -> z; };
}
public int worldZ(int x, int z) {
return minZ + switch (facing) { case 0 -> z; case 1 -> x; case 2 -> depth - 1 - z; default -> width - 1 - x; };
}
public BoundingBox bounds() { return new BoundingBox(minX, minY, minZ, maxX(), maxY(), maxZ()); }
public int startChunkX() { return (minX + worldWidth() / 2) >> 4; }
public int startChunkZ() { return (minZ + worldDepth() / 2) >> 4; }
public BlockPos locator() { return AerialGeometry24.entry(this); }
public boolean merchant() { return kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS; }
public BoundingBox waterfallClaimBounds() {
if (!waterfall) throw new IllegalStateException("This site has no waterfall");
int x = worldX(width / 2, -1), z = worldZ(width / 2, -1);
return new BoundingBox(x - WATERFALL_SPREAD, 0, z - WATERFALL_SPREAD,
x + WATERFALL_SPREAD, AerialGeometry24.waterfallSource(this).getY(), z + WATERFALL_SPREAD);
}
public List<ChunkPos> claimedChunks() {
var result = new LinkedHashSet<ChunkPos>();
addChunks(bounds(), result);
if (waterfall) addChunks(waterfallClaimBounds(), result);
return List.copyOf(result);
}
public boolean ownsChunk(int x, int z) {
return includesChunk(bounds(), x, z) || waterfall && includesChunk(waterfallClaimBounds(), x, z);
}
public boolean mayWrite(BlockPos pos) {
return bounds().isInside(pos) || waterfall && waterfallClaimBounds().isInside(pos);
}
private static boolean includesChunk(BoundingBox bounds, int x, int z) {
return x >= (bounds.minX() >> 4) && x <= (bounds.maxX() >> 4)
&& z >= (bounds.minZ() >> 4) && z <= (bounds.maxZ() >> 4);
}
private static void addChunks(BoundingBox bounds, Set<ChunkPos> output) {
for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++)
for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) output.add(new ChunkPos(x, z));
}
}
}
@@ -0,0 +1,150 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.NoiseColumn;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Kind;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Fixed work limits, not wall-clock-dependent draws. No chunk is requested or modified by this planner. */
public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192;
private AerialPlanner24() {}
public interface Terrain {
NoiseColumn column(int x, int z);
/** Planned structures may add rock above the original noise column or cut its support. */
default boolean reserved(int x, int z, int minY, int maxY) { return false; }
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
if (generator.rootGeneration() != 24) throw new IllegalArgumentException("Aerial planning requires generation24");
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) return AerialPlan24.EMPTY;
generator.prepareAerialSampling24(level);
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
});
}
public static AerialPlan24 plan(long seed, int diameter, Terrain terrain) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported aerial root size");
int radius = diameter == 724 ? 395 : diameter / 2 + 32;
var probe = new Probe(terrain, radius);
var sites = new ArrayList<Site>();
var claimed = new HashSet<Long>();
Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"balloon", "merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
int width = kinds[ordinal] == Kind.BALLOON ? 15 : ordinal < 3 ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width;
int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0);
Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
double angle = initialAngle + ordinal * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
+ (attempt / 16) * .41;
Shore shore = shore(probe, angle, radius);
if (shore == null) continue;
int facing = (int) Math.floorMod(mix(draw), 4);
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
// The full claim remains outside initial terrain ownership, including corner chunks.
double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20;
int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance);
if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
int bottom = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321)
: ordinal < 3 ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
bottom = Math.min(bottom, 350 - height);
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2,
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 3);
if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
return Math.hypot(nearestX, nearestZ) <= radius + 32;
});
if (rootOverlap || !visible(probe, shore, cx, bottom + (ordinal == 0 ? 18 : bodyHeight - 4), cz)) continue;
accepted = site; break;
}
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
sites.add(accepted);
accepted.claimedChunks().forEach(c -> claimed.add(c.pack()));
}
var result = new AerialPlan24(sites);
System.getLogger(AerialPlanner24.class.getName()).log(System.Logger.Level.INFO,
"Aerial24 seed=" + seed + " diameter=" + diameter + " sites=" + result.sites().size()
+ " claims=" + result.claimsChunks().size() + " columns=" + probe.columns.size());
return result;
}
private static Shore shore(Probe probe, double angle, int radius) {
double dx = Math.cos(angle), dz = Math.sin(angle);
// An outside-in scan finds the outer shore, never an internal pond. Refine its edge to a block.
for (int r = radius; r >= 32; r -= 8) {
int x = (int) Math.round(dx * r), z = (int) Math.round(dz * r), top = probe.surface(x, z);
if (top < 120 || top > 326 || probe.terrain.reserved(x, z, top, top + 3)) continue;
Shore best = new Shore(x, top + 1, z);
for (int fine = 1; fine <= 8; fine++) {
int fx = (int) Math.round(dx * (r + fine)), fz = (int) Math.round(dz * (r + fine));
int ft = probe.surface(fx, fz);
if (ft < 120 || ft > 326 || probe.terrain.reserved(fx, fz, ft, ft + 3)) break;
best = new Shore(fx, ft + 1, fz);
}
return best;
}
return null;
}
private static boolean visible(Probe probe, Shore shore, int targetX, int targetY, int targetZ) {
double distance = Math.hypot(targetX - shore.x, targetZ - shore.z);
int steps = (int) Math.ceil(distance);
for (int step = 1; step < steps; step++) {
double t = (double) step / steps;
int x = (int) Math.floor(shore.x + .5 + (targetX - shore.x) * t);
int z = (int) Math.floor(shore.z + .5 + (targetZ - shore.z) * t);
int y = (int) Math.floor(shore.y + 2.0 + (targetY - shore.y - 2.0) * t);
if (probe.solid(x, y, z) || probe.terrain.reserved(x, z, y, y)) return false;
}
return true;
}
private record Shore(int x, int y, int z) {}
private static final class Probe {
final Terrain terrain; final int radius;
final Map<Long, NoiseColumn> columns = new HashMap<>();
Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; }
NoiseColumn column(int x, int z) {
if (Math.hypot(x, z) >= radius) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var present = columns.get(key); if (present != null) return present;
if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted");
var result = terrain.column(x, z);
if (result == null) throw new IllegalStateException("Aerial terrain query returned no column");
columns.put(key, result); return result;
}
int surface(int x, int z) {
var column = column(x, z); if (column == null) return 0;
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
return 0;
}
boolean solid(int x, int y, int z) {
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
return value ^ value >>> 31;
}
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
}
@@ -0,0 +1,276 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import fr.koka.sanctuary.worldgen.detail.ConnectedBlocks20;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.storage.loot.LootTable;
/** Complete supported silhouettes; no terrain pillars, remote writes, or fabricated flowing-water columns. */
public final class AerialRenderer24 {
private AerialRenderer24() {}
/** Export actual final block states for inspection, including rotations and generated chest positions. */
public static void writeDiagnostic(Path path, List<AerialPlan24.Site> sites) throws IOException {
var result = new ArrayList<Map<String, Object>>();
for (var site : sites) {
var blocks = blocks(site);
var list = blocks.entrySet().stream().filter(e -> !e.getValue().isAir()).map(e -> {
var p = e.getKey(); var state = e.getValue();
return List.of(p.getX() - site.minX(), p.getY() - site.minY(), p.getZ() - site.minZ(),
BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString());
}).toList();
result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()),
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list));
}
if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n");
}
public static Map<BlockPos, BlockState> blocks(AerialPlan24.Site site) {
var blocks = new LinkedHashMap<BlockPos, BlockState>();
render(site, new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
});
return Map.copyOf(blocks);
}
public static ResourceKey<LootTable> loot(AerialPlan24.Site s) {
return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/aerial24/" + s.kind().name().toLowerCase(java.util.Locale.ROOT)));
}
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out) { render(s, out, s.bounds()); }
public static void render(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
// Connections see the complete airship before the native destination clips its own chunk.
// Mineral columns need no connection buffer and keep their bounded per-chunk scan.
LostCityRenderer.BlockWriter clipped = new LostCityRenderer.BlockWriter() {
@Override public void set(int x, int y, int z, BlockState state) {
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.set(x, y, z, state);
}
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
if (clip.isInside(s.minX() + x, s.minY() + y, s.minZ() + z)) out.chest(x, y, z, table, seed);
}
};
var connected = s.kind() == AerialPlan24.Kind.MINERAL_ISLET ? null : ConnectedBlocks20.buffer(clipped);
var destination = connected == null ? clipped : connected;
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
switch (s.kind()) {
case BALLOON -> b.balloon();
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
case MINERAL_ISLET -> b.islet();
}
var chest = AerialGeometry24.chest(s);
destination.chest(chest.getX() - s.minX(), chest.getY() - s.minY(), chest.getZ() - s.minZ(), loot(s), s.seed() ^ 0x24C4E57L);
if (connected != null) connected.flush();
}
private static final class Builder {
final AerialPlan24.Site s;
final LostCityRenderer.BlockWriter out;
final BoundingBox clip;
final Rotation rotation;
Builder(AerialPlan24.Site s, LostCityRenderer.BlockWriter out, BoundingBox clip) {
this.s = s; this.out = out; this.clip = clip;
rotation = switch (s.facing()) { case 1 -> Rotation.CLOCKWISE_90; case 2 -> Rotation.CLOCKWISE_180; case 3 -> Rotation.COUNTERCLOCKWISE_90; default -> Rotation.NONE; };
}
void set(int x, int y, int z, Block block) { set(x, y, z, block.defaultBlockState()); }
void set(int x, int y, int z, BlockState state) {
if (x < 0 || y < 0 || z < 0 || x >= s.width() || y >= s.height() || z >= s.depth()) return;
int wx = s.worldX(x, z), wz = s.worldZ(x, z);
if (clip.isInside(wx, s.minY() + y, wz)) out.set(wx - s.minX(), y, wz - s.minZ(), state.rotate(rotation));
}
void box(int x0, int y0, int z0, int x1, int y1, int z1, Block block) {
for (int x = x0; x <= x1; x++) for (int z = z0; z <= z1; z++) for (int y = y0; y <= y1; y++) set(x, y, z, block);
}
void slab(int x, int y, int z) { set(x, y, z, Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE, SlabType.BOTTOM)); }
void stair(int x, int y, int z, Direction direction, Half half) {
set(x, y, z, Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction).setValue(BlockStateProperties.HALF, half));
}
void lantern(int x, int y, int z) { set(x, y, z, Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true)); }
void door(int x, int y, int z, Direction direction) {
var state = Blocks.OAK_DOOR.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, direction);
set(x, y, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.LOWER));
set(x, y + 1, z, state.setValue(BlockStateProperties.DOUBLE_BLOCK_HALF, DoubleBlockHalf.UPPER));
}
void bed(int x, int y, int z) {
var state = Blocks.BED.white().defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH);
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
}
void balloon() {
int cx = s.width() / 2, cz = s.depth() / 2;
// The envelope is a rounded shell. Continuous spars attach the four real suspensions.
Block accent = (s.seed() & 1) == 0 ? Blocks.WOOL.yellow() : Blocks.WOOL.lightBlue();
for (int y = 9; y <= 27; y++) for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
double q = Math.pow((x - cx) / 7.1, 2) + Math.pow((z - cz) / 9.1, 2) + Math.pow((y - 18) / 9.2, 2);
if (q <= 1.02 && q >= .73) set(x, y, z, Math.abs(x - cx) <= 1 || y == 19 ? accent : y % 6 == 0 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
}
box(cx - 3, 0, cz - 4, cx + 3, 1, cz + 4, Blocks.OAK_PLANKS);
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 4; z <= cz + 4; z++) {
if (Math.abs(x - cx) == 3 || Math.abs(z - cz) == 4) {
set(x, 2, z, Blocks.OAK_PLANKS); slab(x, 3, z);
}
}
// One entry faces a usable standing deck, with a removable railing for boarding.
set(cx, 2, cz - 4, Blocks.OAK_FENCE_GATE); set(cx, 3, cz - 4, Blocks.AIR);
for (int x : new int[]{cx - 3, cx + 3}) for (int z : new int[]{cz - 4, cz + 4}) {
box(x, 3, z, x, 5, z, Blocks.STRIPPED_OAK_LOG);
box(x, 6, z, x, 12, z, Blocks.IRON_CHAIN);
}
for (int z : new int[]{cz - 4, cz + 4}) box(cx - 4, 12, z, cx + 4, 12, z, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{cx - 3, cx + 3}) box(x, 12, cz - 5, x, 12, cz + 5, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 6, cz, cx + 3, 6, cz, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 5, cz - 4, cx - 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
box(cx + 3, 5, cz - 4, cx + 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
set(cx, 5, cz, Blocks.COPPER_BULB.waxed().unaffected().defaultBlockState().setValue(BlockStateProperties.LIT, true));
lantern(cx - 2, 5, cz); lantern(cx + 2, 5, cz);
set(cx + 2, 2, cz + 2, Blocks.CRAFTING_TABLE);
set(cx - 2, 2, cz - 2, Blocks.BARREL);
set(cx + 2, 2, cz - 2, Blocks.HAY_BLOCK);
}
int halfWidth(int y, int z) {
int end = Math.min(z, s.depth() - 1 - z);
return Math.max(0, Math.min(5, Math.min(1 + end, 1 + y)));
}
void ship() {
int cx = 5;
for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) {
int half = halfWidth(y, z);
for (int x = cx - half; x <= cx + half; x++) {
boolean hull = y == 0 || y == 5 || Math.abs(x - cx) == half || z < 2 || z >= 25;
set(x, y, z, hull ? (y == 0 ? Blocks.STRIPPED_OAK_LOG : Blocks.OAK_PLANKS) : Blocks.AIR);
}
}
// Curved gunwales remain closed; the wide centre stays clear for boarding small boats.
for (int z = 1; z <= 25; z++) {
int half = halfWidth(5, z);
for (int side : new int[]{-1, 1}) {
int x = cx + side * half;
set(x, 6, z, Blocks.OAK_FENCE); // Connection properties are completed below.
if (z % 5 == 0) set(x, 6, z, Blocks.STRIPPED_OAK_LOG);
}
}
for (int x = 3; x <= 7; x++) { set(x, 6, 1, Blocks.OAK_FENCE); set(x, 6, 25, Blocks.OAK_FENCE); }
// Roof, walls, threshold and cabin circulation share one continuous frame.
box(2, 6, 17, 8, 9, 24, Blocks.OAK_PLANKS);
box(3, 6, 18, 7, 9, 23, Blocks.AIR);
box(2, 10, 17, 8, 10, 24, Blocks.OAK_PLANKS);
for (int z = 17; z <= 24; z++) {
stair(1, 10, z, Direction.EAST, Half.BOTTOM); stair(9, 10, z, Direction.WEST, Half.BOTTOM);
}
for (int x : new int[]{2, 8}) for (int z : new int[]{17, 24}) box(x, 6, z, x, 10, z, Blocks.STRIPPED_OAK_LOG);
for (int z = 19; z <= 21; z++) for (int y = 7; y <= 8; y++) { set(2, y, z, Blocks.GLASS); set(8, y, z, Blocks.GLASS); }
for (int x = 4; x <= 6; x++) set(x, 8, 24, Blocks.GLASS);
door(5, 6, 17, Direction.SOUTH);
bed(3, 6, 22);
set(7, 6, 19, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
set(3, 6, 19, Blocks.CRAFTING_TABLE); set(3, 6, 20, Blocks.BARREL);
lantern(5, 9, 20); lantern(5, 9, 23);
// Cale reached by an actual supported stair run; no disconnected decorative floor.
for (int step = 0; step < 4; step++) {
int z = 10 + step, y = 4 - step;
box(5, 1, z, 5, y, z, Blocks.OAK_PLANKS);
box(5, y + 1, z, 5, 6, z, Blocks.AIR);
stair(5, y, z, Direction.NORTH, Half.BOTTOM);
}
box(3, 1, 7, 7, 1, 20, Blocks.OAK_PLANKS);
set(3, 2, 17, Blocks.BARREL); set(7, 2, 17, Blocks.BARREL);
lantern(3, 4, 8); lantern(7, 4, 8);
Block cargo = s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.HAY_BLOCK : Blocks.IRON_BLOCK;
box(2, 6, 5, 3, 7, 7, cargo); box(7, 6, 5, 8, 6, 7, Blocks.BARREL);
box(5, 6, 4, 5, 16, 4, Blocks.STRIPPED_OAK_LOG);
for (int x = 1; x <= 9; x++)
set(x, 16, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
for (int x = 2; x <= 8; x++)
set(x, 8, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
set(5, 17, 4, Blocks.LANTERN);
// Seven by seven sail around the continuous mast: yard above, boom below, clear deck beneath.
for (int y = 9; y <= 15; y++) for (int x = 2; x <= 8; x++)
if (x != cx) set(x, y, 4, y == 11 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
// Stable handrails are explicit, rather than depending on neighbouring chunk updates.
for (int z = 2; z <= 24; z++) for (int x : new int[]{0, 10}) if (halfWidth(5, z) == 5 && z % 5 != 0 && !(x == 0 && z == 12))
set(x, 6, z, Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.NORTH, true).setValue(BlockStateProperties.SOUTH, true));
}
void islet() {
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
if (!clip.isInside(s.worldX(x, z), s.minY(), s.worldZ(x, z))
&& (s.worldX(x,z) < clip.minX() || s.worldX(x,z) > clip.maxX() || s.worldZ(x,z) < clip.minZ() || s.worldZ(x,z) > clip.maxZ())) continue;
int top = AerialGeometry24.terrainTop(s, x, z);
for (int y = 0; y <= top; y++) if (AerialGeometry24.mineralBody(s, x, y, z)) set(x, y, z, ore(x, y, z, top));
if (AerialGeometry24.naturalSurface(s, x, z)) {
set(x, top, z, Blocks.GRASS_BLOCK);
int soil = 2 + (int) Math.floorMod(AerialGeometry24.mix(s.seed() ^ x * 19349663L ^ z * 83492791L), 2);
for (int layer = 1; layer <= soil && top - layer > AerialGeometry24.terrainBottom(s, x, z); layer++)
if (AerialGeometry24.mineralBody(s, x, top - layer, z)) set(x, top - layer, z, Blocks.DIRT);
if (!AerialGeometry24.arrivalColumn(s, x, z)) {
long foliage = AerialGeometry24.mix(s.seed() ^ x * 374761393L ^ z * 668265263L);
int roll = (int) Math.floorMod(foliage, 100);
if (roll < 8) set(x, top + 1, z, switch ((int) Math.floorMod(foliage >>> 16, 4)) {
case 0 -> Blocks.DANDELION; case 1 -> Blocks.POPPY; case 2 -> Blocks.OXEYE_DAISY; default -> Blocks.CORNFLOWER;
});
else if (roll < 44) set(x, top + 1, z, Blocks.SHORT_GRASS);
}
}
}
if (s.waterfall()) {
int cx = s.width() / 2, floor = AerialGeometry24.rockHeight(s) - 4;
for (int z = 0; z <= 4; z++) {
set(cx, floor, z, Blocks.STONE);
set(cx - 1, floor + 1, z, Blocks.STONE); set(cx + 1, floor + 1, z, Blocks.STONE);
box(cx, floor + 1, z, cx, floor + 3, z, Blocks.AIR);
}
set(cx, floor + 1, 4, Blocks.STONE);
set(cx, floor + 1, 3, Blocks.WATER);
}
// A true buried chest: intact roof and floor, no decorative open loot pedestal.
var c = AerialGeometry24.chest(s);
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2, y = c.getY() - s.minY();
box(x - 1, y - 1, z - 1, x + 1, y + 2, z + 1, Blocks.STONE);
for (var tree : AerialGeometry24.trees(s)) tree(tree);
}
void tree(AerialGeometry24.Tree tree) {
Block leaves = tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES;
int lastLog = tree.groundY() + tree.trunkHeight();
for (int y = lastLog - 2; y <= lastLog + 1; y++) {
int radius = y < lastLog ? 2 : 1;
for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) + Math.abs(dz) > (radius == 2 ? 3 : 1)) continue;
int distance = Math.max(1, Math.abs(dx) + Math.abs(dz) + Math.max(0, y - lastLog));
set(tree.x() + dx, y, tree.z() + dz, leaves.defaultBlockState()
.setValue(BlockStateProperties.DISTANCE, distance).setValue(BlockStateProperties.PERSISTENT, false));
}
}
box(tree.x(), tree.groundY() + 1, tree.z(), tree.x(), lastLog, tree.z(), tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG);
}
Block ore(int x, int y, int z, int top) {
double depth = (double) (top - y) / Math.max(1, top);
long cell = AerialGeometry24.mix(s.seed() ^ ((long)(x / 4) << 42) ^ ((long)(z / 4) << 21) ^ (y / 3));
int roll = (int) Math.floorMod(cell, 100);
// Relative strata make the lower suspended rock useful without pretending it is y=-64.
if (roll < 37) {
if (depth < .27) return roll < 15 ? Blocks.COAL_ORE : roll < 27 ? Blocks.COPPER_ORE : Blocks.IRON_ORE;
if (depth < .60) return roll < 13 ? Blocks.IRON_ORE : roll < 21 ? Blocks.GOLD_ORE : roll < 29 ? Blocks.LAPIS_ORE : Blocks.COAL_ORE;
return roll < 10 ? Blocks.DEEPSLATE_IRON_ORE : roll < 19 ? Blocks.DEEPSLATE_REDSTONE_ORE : roll < 27 ? Blocks.DEEPSLATE_GOLD_ORE : roll < 34 ? Blocks.DEEPSLATE_DIAMOND_ORE : Blocks.DEEPSLATE_EMERALD_ORE;
}
if (depth > .6) return roll < 59 ? Blocks.TUFF : Blocks.DEEPSLATE;
return roll < 53 ? Blocks.ANDESITE : roll < 59 ? Blocks.COBBLESTONE : Blocks.STONE;
}
}
}
@@ -0,0 +1,166 @@
package fr.koka.sanctuary.worldgen.aerial;
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.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.Set;
import java.util.function.Supplier;
/** Content-addressed initial layout. Its hash is authoritative only once bound by the expansion journal24. */
public final class AerialStore24 {
private static final int MAX_BYTES = 32768;
private static final Set<String> ROOT_FIELDS = Set.of("schema", "world_seed", "root_diameter", "generation", "sites");
private static final Set<String> SITE_FIELDS = Set.of("id", "kind", "min_x", "min_y", "min_z", "width", "height", "depth",
"facing", "seed", "shore_x", "shore_y", "shore_z", "waterfall");
private AerialStore24() {}
public record Stored(String token, AerialPlan24 plan) {}
/** Store lives beside sanctuary-world-v24, so creating an orphan manifest never looks like an initialized journal. */
public static Path directory(Path journalDirectory) { return journalDirectory.resolveSibling("sanctuary-aerial-v24"); }
public static Stored create(Path directory, long seed, int diameter, Supplier<AerialPlan24> factory) throws IOException {
AerialPlan24 plan = factory.get();
validateExtent(plan, diameter);
String encoded = encode(plan, seed, diameter).toString() + "\n";
byte[] bytes = encoded.getBytes(StandardCharsets.UTF_8);
if (bytes.length > MAX_BYTES) throw new IOException("Aerial layout exceeds its size limit");
String token = digest(bytes);
if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS))
throw new IOException("Aerial manifest path is not a directory");
Files.createDirectories(directory);
Path file = directory.resolve(token + ".json");
if (!Files.exists(file, LinkOption.NOFOLLOW_LINKS)) {
Path temporary = Files.createTempFile(directory, ".aerial-", ".tmp");
try {
try (FileChannel output = FileChannel.open(temporary, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
var buffer = ByteBuffer.wrap(bytes); while (buffer.hasRemaining()) output.write(buffer); output.force(true);
}
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE);
if (!file.getFileSystem().getSeparator().equals("\\")) {
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
}
} finally { Files.deleteIfExists(temporary); }
}
Stored verified = load(directory, token, seed, diameter);
if (!verified.plan().sites().equals(plan.sites())) throw new IOException("Aerial manifest read-back differs from its plan");
return verified;
}
public static Stored load(Path directory, String token, long seed, int diameter) throws IOException {
if (token == null || !token.matches("[0-9a-f]{64}")) throw new IOException("Invalid aerial manifest token");
Path file = directory.resolve(token + ".json");
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS) || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|| Files.size(file) > MAX_BYTES) throw new IOException("Missing or invalid saved aerial manifest");
byte[] bytes = Files.readAllBytes(file);
if (!digest(bytes).equals(token)) throw new IOException("Saved aerial manifest has changed");
try (var reader = new JsonReader(new StringReader(new String(bytes, StandardCharsets.UTF_8)))) {
reader.setStrictness(Strictness.STRICT);
JsonObject root = readJson(reader, 0).getAsJsonObject();
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing aerial manifest data");
fields(root, ROOT_FIELDS);
if (integer(root, "schema") != 1 || integer(root, "generation") != 24
|| number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != diameter)
throw new IOException("Aerial manifest differs from its world contract");
var sites = new ArrayList<AerialPlan24.Site>();
for (JsonElement item : root.getAsJsonArray("sites")) {
JsonObject site = item.getAsJsonObject(); fields(site, SITE_FIELDS);
sites.add(new AerialPlan24.Site(string(site, "id"), AerialPlan24.Kind.valueOf(string(site, "kind")),
integer(site, "min_x"), integer(site, "min_y"), integer(site, "min_z"), integer(site, "width"),
integer(site, "height"), integer(site, "depth"), integer(site, "facing"), number(site, "seed").longValueExact(),
integer(site, "shore_x"), integer(site, "shore_y"), integer(site, "shore_z"), bool(site, "waterfall")));
}
var plan = new AerialPlan24(sites); validateExtent(plan, diameter);
return new Stored(token, plan);
} catch (RuntimeException error) { throw new IOException("Invalid aerial manifest", error); }
}
private static void validateExtent(AerialPlan24 plan, int diameter) throws IOException {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IOException("Unsupported aerial root size");
int radius = (diameter == 724 ? 395 : diameter / 2 + 32) + 32;
for (long packed : plan.claimsChunks()) {
var c = net.minecraft.world.level.ChunkPos.unpack(packed);
int x = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
int z = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
}
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
var root = new JsonObject();
root.addProperty("schema", 1); root.addProperty("world_seed", seed); root.addProperty("root_diameter", diameter); root.addProperty("generation", 24);
var sites = new JsonArray();
for (var s : plan.sites()) {
var site = new JsonObject(); site.addProperty("id", s.id()); site.addProperty("kind", s.kind().name());
site.addProperty("min_x", s.minX()); site.addProperty("min_y", s.minY()); site.addProperty("min_z", s.minZ());
site.addProperty("width", s.width()); site.addProperty("height", s.height()); site.addProperty("depth", s.depth());
site.addProperty("facing", s.facing()); site.addProperty("seed", s.seed());
site.addProperty("shore_x", s.shoreX()); site.addProperty("shore_y", s.shoreY()); site.addProperty("shore_z", s.shoreZ());
site.addProperty("waterfall", s.waterfall()); sites.add(site);
}
root.add("sites", sites); return root;
}
private static String digest(byte[] bytes) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); }
}
private static void fields(JsonObject object, Set<String> expected) throws IOException {
if (!object.keySet().equals(expected)) throw new IOException("Unexpected aerial manifest fields");
}
private static String string(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isString()) throw new IllegalArgumentException("Expected aerial text");
return value.getAsString();
}
private static BigDecimal number(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isNumber()) throw new IllegalArgumentException("Expected aerial number");
return value.getAsBigDecimal();
}
private static int integer(JsonObject object, String key) { return number(object, key).intValueExact(); }
private static boolean bool(JsonObject object, String key) {
var value = object.getAsJsonPrimitive(key); if (!value.isBoolean()) throw new IllegalArgumentException("Expected aerial boolean");
return value.getAsBoolean();
}
private static JsonElement readJson(JsonReader reader, int depth) throws IOException {
if (depth > 3) throw new IOException("Aerial manifest exceeds its nesting limit");
return switch (reader.peek()) {
case BEGIN_OBJECT -> {
reader.beginObject(); var object = new JsonObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (object.has(name) || object.size() >= 16) throw new IOException("Duplicate or excessive aerial fields");
object.add(name, readJson(reader, depth + 1));
}
reader.endObject(); yield object;
}
case BEGIN_ARRAY -> {
reader.beginArray(); var array = new JsonArray();
while (reader.hasNext()) {
if (array.size() >= 5) throw new IOException("Too many aerial sites");
array.add(readJson(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("Invalid aerial JSON value");
};
}
}
@@ -0,0 +1,35 @@
package fr.koka.sanctuary.worldgen.aerial;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.Optional;
import java.util.Set;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
/** Each family uses its own native registry identity, independent of any terrain-island owner. */
public final class AerialStructure24 extends Structure {
public static final MapCodec<AerialStructure24> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
settingsCodec(instance), Codec.STRING.fieldOf("family").forGetter(AerialStructure24::family)
).apply(instance, AerialStructure24::new));
private final String family;
public AerialStructure24(StructureSettings settings, String family) {
super(settings);
if (!Set.of("balloon", "merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
this.family = family;
}
public String family() { return family; }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator) || generator.rootGeneration() != 24
|| !generator.experimentalStructures()) return Optional.empty();
var sites = generator.aerialPlan24().atStartChunk(context.chunkPos().x(), context.chunkPos().z()).stream()
.filter(site -> AerialStructures24.family(site).equals(family)).toList();
if (sites.isEmpty()) return Optional.empty();
return Optional.of(new GenerationStub(AerialGeometry24.entry(sites.getFirst()), builder -> {
for (var site : sites) builder.addPiece(new AerialPiece24(site));
}));
}
@Override public StructureType<?> type() { return AerialStructures24.TYPE; }
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
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.world.level.Level;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.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.pieces.StructurePieceType;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
/** Native starts for five immutable initial sites. Their void chunks need no terrain island owner. */
public final class AerialStructures24 {
public static final ResourceKey<Structure> BALLOON = key("aerial_balloon");
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
public static final List<ResourceKey<Structure>> KEYS = List.of(BALLOON, MERCHANT_SHIP, MINERAL_ISLET);
public static final StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("aerial_piece_v24"), AerialPiece24::new);
private static final ThreadLocal<AerialPlan24.Site> PLACING = new ThreadLocal<>();
private AerialStructures24() {}
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {}
public static String family(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> "balloon"; case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
}
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> BALLOON; case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
}
public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
public static boolean isPlacing() { return PLACING.get() != null; }
public static boolean mayWrite(BlockPos pos) { var site = PLACING.get(); return site != null && site.bounds().isInside(pos); }
static void placing(AerialPlan24.Site site, Runnable action) {
var old = PLACING.get(); PLACING.set(site);
try { action.run(); } finally { if (old == null) PLACING.remove(); else PLACING.set(old); }
}
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() != 24
|| !sanctuary.experimentalStructures()) return;
var sites = sanctuary.aerialPlan24().atStartChunk(chunk.getPos().x(), chunk.getPos().z());
if (sites.isEmpty()) return;
// Climate evaluation may use the original island's random state; geometry belongs only to the saved site.
var origin = generator.source().islands().stream().filter(i -> i.origin()).findFirst().orElseThrow();
var random = generator.randomState(origin);
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
for (var key : sites.stream().map(AerialStructures24::key).distinct().toList()) {
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(key);
Structure structure = holder.value();
StructureStart previous = manager.getStartForStructure(structure, chunk);
if (previous != null && previous.isValid()) continue;
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, origin.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, structure.biomes()::contains);
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
}
}
@@ -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.rootGeneration() == 23) && 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.rootGeneration() == 24)) && generator.cityReservation16(island) != null
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || generator.cityPlacement21(island).core().surface().districts().contains(district))) && ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || 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 || generator.rootGeneration() == 23)) for (var support : generator.citySupports16(island)) { if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) 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 || 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 district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece21(parcel) : generator.rootGeneration() == 20 ? new LostCityPiece20(parcel) : (generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
? 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 || generator.rootGeneration() == 23)) for (var network : generator.undergroundPlan16(island).networks()) { if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) 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 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m))); .forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? 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 || generator.rootGeneration() == 23) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m))); .forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) ? new LostCityUndergroundPiece20(m) : new LostCityUndergroundPiece16(m)));
} }
})); }));
} }
@@ -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 && generator.rootGeneration() != 23)) return Optional.empty(); || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) 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 && island.generation() != 23)) return Optional.empty(); if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) 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();
@@ -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 && generator.rootGeneration() != 23)) return; || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) 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 && island.generation() != 23)) return; if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24))) 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 && generator.rootGeneration() != 23) || !level.structureManager().shouldGenerateStructures()) return null; || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) || !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.generation() != 23) || !island.status().equals("ready")) continue; if (island == null || !island.origin() || (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)) || !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);
} }
@@ -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() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) { || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) {
source.sendFailure(Component.translatable("sanctuary.heritage.unavailable")); source.sendFailure(Component.translatable("sanctuary.heritage.unavailable"));
return 0; return 0;
} }
@@ -28,20 +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 || generator.rootGeneration() == 23)) for (var module : generator.transitPlan21().modules()) { if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) 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 || generator.rootGeneration() == 23)) for (var network : generator.undergroundHall22().networks()) { if ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) 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);
} }
if (generator.rootGeneration() == 23 && !generator.mineralSanctuary23().isEmpty()) { if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty()) {
var at = generator.mineralSanctuary23().chamber().locator(); var at = generator.mineralSanctuary23().chamber().locator();
source.sendSuccess(() -> Component.translatable("sanctuary.mineral_sanctuary.entry", at.getX(), at.getY(), at.getZ()), false); 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) return plan.sites().size() + ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24) && !generator.mineralSanctuary23().isEmpty() ? 1 : 0) + ((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
? (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);
})))); }))));
@@ -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.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !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 || generator.rootGeneration() == 23)) ? new HeritagePiece21(segment) : generator.rootGeneration() == 20 ? new HeritagePiece20(segment) : new HeritagePiece19(segment)); pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? 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 || generator.rootGeneration() == 23)) ? new HeritagePiece21(site) : generator.rootGeneration() == 20 ? new HeritagePiece20(site) : new HeritagePiece19(site)); pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? 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();
@@ -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.rootGeneration() != 23)) || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !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.rootGeneration() != 23)) || !generator.experimentalStructures() || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !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;
@@ -11,9 +11,9 @@ public final class MineralSanctuaryStructure23 extends Structure {
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); } public MineralSanctuaryStructure23(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() != 23 || !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !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() != 23) return Optional.empty(); if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return Optional.empty();
var plan = generator.mineralSanctuary23(); var plan = generator.mineralSanctuary23();
if (plan.isEmpty()) return Optional.empty(); if (plan.isEmpty()) return Optional.empty();
var c = plan.chamber(); var c = plan.chamber();
@@ -40,9 +40,9 @@ public final class MineralSanctuaryStructures23 {
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() != 23 || !sanctuary.experimentalStructures()) return; || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !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() != 23) return; if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24)) return;
var plan = sanctuary.mineralSanctuary23(); var plan = sanctuary.mineralSanctuary23();
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x() if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return; || plan.chamber().startChunkZ() != chunk.getPos().z()) return;
@@ -63,14 +63,14 @@ public final class MineralSanctuaryStructures23 {
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() != 23 || !level.structureManager().shouldGenerateStructures()) return null; || (generator.rootGeneration() != 23 && generator.rootGeneration() != 24) || !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 plan = generator.mineralSanctuary23(); var plan = generator.mineralSanctuary23();
if (plan.isEmpty() || !generator.experimentalStructures()) return null; if (plan.isEmpty() || !generator.experimentalStructures()) return null;
var c = plan.chamber(); var c = plan.chamber();
var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ()); var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ());
if (island == null || !island.origin() || island.generation() != 23 || !island.status().equals("ready")) return null; if (island == null || !island.origin() || (island.generation() != 23 && island.generation() != 24) || !island.status().equals("ready")) return null;
if (Math.abs((long) c.startChunkX() - (origin.getX() >> 4)) > radius if (Math.abs((long) c.startChunkX() - (origin.getX() >> 4)) > radius
|| Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null; || Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null;
var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true); var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { @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.rootGeneration() != 23)) || !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !generator.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 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r)))); .forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? 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 || generator.rootGeneration() == 23)) ? new RuinsPiece20(r) : new RuinsPiece17(r)))); .forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))) ? new RuinsPiece20(r) : new RuinsPiece17(r))));
})); }));
} }
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; } @Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
@@ -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.rootGeneration() != 23)) || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| !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.rootGeneration() != 23)) || !generator.experimentalStructures() || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || !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;
@@ -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() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures()) return Optional.empty(); || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !generator.experimentalStructures()) return Optional.empty();
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 && island.generation() != 23))) return Optional.empty(); if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) 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 || generator.rootGeneration() == 23) modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& 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)))));
} }
@@ -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.rootGeneration() != 23)) || !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !sanctuary.experimentalStructures()) return; || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !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 && island.generation() != 23))) return; if (island == null || !island.origin() || (island.generation() != 21 && (island.generation() != 22 && (island.generation() != 23 && island.generation() != 24)))) 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() != 23)) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !generator.experimentalStructures() || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24))) || (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !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;
@@ -225,5 +225,17 @@
"sanctuary.structures.census.unknown_dimension": "Unknown dimension: %s", "sanctuary.structures.census.unknown_dimension": "Unknown dimension: %s",
"sanctuary.structures.census.status.incompatible": "Incompatible habitat", "sanctuary.structures.census.status.incompatible": "Incompatible habitat",
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension", "sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s" "sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.",
"sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.",
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · starts %s/%s · features %s/%s · full chunks %s/%s",
"sanctuary.structures.census.aerial.status.planned": "planned",
"sanctuary.structures.census.aerial.status.accepted": "placement accepted",
"sanctuary.structures.census.aerial.status.generated": "generated",
"sanctuary.structures.census.aerial.status.missing": "start missing after verification",
"sanctuary.structures.census.aerial.status.unknown": "unknown",
"structure.sanctuary.aerial_balloon": "Hot air balloon",
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
"structure.sanctuary.aerial_mineral_islet": "Mineral islet"
} }
@@ -225,5 +225,17 @@
"sanctuary.structures.census.unknown_dimension": "Dimension inconnue : %s", "sanctuary.structures.census.unknown_dimension": "Dimension inconnue : %s",
"sanctuary.structures.census.status.incompatible": "Milieu incompatible", "sanctuary.structures.census.status.incompatible": "Milieu incompatible",
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension", "sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s" "sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.",
"sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.",
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · départs %s/%s · décoration %s/%s · chunks complets %s/%s",
"sanctuary.structures.census.aerial.status.planned": "planifié",
"sanctuary.structures.census.aerial.status.accepted": "implantation acceptée",
"sanctuary.structures.census.aerial.status.generated": "généré",
"sanctuary.structures.census.aerial.status.missing": "départ absent après vérification",
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
"structure.sanctuary.aerial_balloon": "Montgolfière",
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral"
} }
@@ -0,0 +1,127 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:spyglass"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:gold_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 4
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lead",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 4,
"max": 8
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
}
]
}
@@ -0,0 +1,176 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:carrot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:potato",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:wheat_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:pumpkin_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:melon_seeds",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:cooked_beef",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:golden_apple",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 2
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
}
]
}
@@ -0,0 +1,146 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_pickaxe"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_axe"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_shovel"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:copper_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:coal",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 40
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 16,
"max": 32
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 5
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
}
]
}
@@ -0,0 +1,157 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 3,
"max": 7
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 20
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:gold_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:iron_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lapis_lazuli",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:redstone",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 24,
"max": 48
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:amethyst_shard",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:golden_apple",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 1,
"max": 3
}
}
]
}
]
}
]
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "balloon",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "merchant_ship",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,10 @@
{
"type": "sanctuary:aerial_v24",
"family": "mineral_islet",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -3,7 +3,7 @@
"minecraft:overworld": { "minecraft:overworld": {
"type": "sanctuary:sanctuary_384", "type": "sanctuary:sanctuary_384",
"generator": { "generator": {
"type": "sanctuary:island_v23", "type": "sanctuary:island_v24",
"biome_source": { "biome_source": {
"biomes": [ "biomes": [
"minecraft:the_void", "minecraft:the_void",
@@ -64,7 +64,7 @@
"minecraft:snowy_beach", "minecraft:snowy_beach",
"minecraft:stony_shore" "minecraft:stony_shore"
], ],
"type": "sanctuary:island_v23" "type": "sanctuary:island_v24"
}, },
"initial_diameter": 724 "initial_diameter": 724
} }
@@ -0,0 +1,94 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"generator": {
"type": "sanctuary:island_v23",
"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",
"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
}
},
"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"
}
}
}
}
@@ -0,0 +1,170 @@
package fr.koka.sanctuary.worldgen.aerial;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import java.util.HashSet;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.storage.loot.LootTable;
/** Pure finite planning witnesses: nine size/seed combinations, rotation and pre-visit claims. */
public final class Aerial24Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
BlockState[] column = new BlockState[384];
for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState();
for (int size : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42, 2026}) {
int[] queries = {0};
var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); });
var replay = AerialPlanner24.plan(seed, size, (x, z) -> new NoiseColumn(0, column));
require(plan.sites().equals(replay.sites()) && plan.claimsChunks().equals(replay.claimsChunks()), "Plan does not depend on time or loaded chunks");
require(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget");
require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Five complete sites, including two distinct merchant ships");
require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
var allClaims = new HashSet<Long>();
var root = ExpansionIsland.origin(size, seed, 24);
for (var site : plan.sites()) {
var physicalChunks = new HashSet<>(site.bounds().intersectingChunks().toList());
if (site.waterfall()) physicalChunks.addAll(site.waterfallClaimBounds().intersectingChunks().toList());
require(physicalChunks.equals(new HashSet<>(site.claimedChunks())),
"Claim contains only body chunks and the local waterfall outlet, without a generic margin");
require(site.maxY() < 350 && site.minY() >= 32, "The entire site fits the dimension height");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
require(site.rockHeight() >= 20 && site.rockHeight() <= 35
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
"Vegetation adds eight blocks of headroom without enlarging the stone body");
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
require(distance <= 512, "Every site has a nearby shore witness");
require(plan.atStartChunk(site.startChunkX(), site.startChunkZ()).contains(site), "Native departure belongs to its saved site");
for (var chunk : site.claimedChunks()) {
require(allClaims.add(chunk.pack()), "Site claims do not overlap");
require(site.ownsChunk(chunk.x(), chunk.z()) && plan.ownsChunk(chunk.x(), chunk.z()), "Claim query agrees with the immutable mask");
require(!root.ownsChunk(chunk.x(), chunk.z()), "Claim does not replace initial island ownership");
}
var overlapping = new ExpansionIsland("overlap", "sanctuary", "east", "temperate", 64,
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
require(plan.conflicts(overlapping), "Expansion conflicts are known before visiting or generating a site");
var local = new HashSet<BlockPos>();
for (int face = 0; face < 4; face++) {
var rotated = rotate(site, face); local.clear();
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
var at = new BlockPos(rotated.worldX(x, z), site.minY(), rotated.worldZ(x, z));
require(rotated.bounds().isInside(at) && rotated.mayWrite(at), "Rotations stay inside their world bounds");
require(local.add(at), "Rotation never folds two local columns together");
}
}
require(!site.mayWrite(new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)), "No writes outside the finite envelope");
require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation");
if (seed == 0) verifyRender(site);
}
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the five finite site masks");
var far = new ExpansionIsland("far", "sanctuary", "east", "temperate", 64, 4096, 4096, 1, "natural", "reserved", 24);
require(!plan.conflicts(far), "Aerial sites do not reserve an infinite ring around Sanctuary");
require(!plan.conflicts(root), "Root ownership is not treated as a future expansion");
try { plan.sites().clear(); throw new AssertionError("Mutable site list"); }
catch (UnsupportedOperationException expected) { }
try { plan.claimsChunks().clear(); throw new AssertionError("Mutable claims"); }
catch (UnsupportedOperationException expected) { }
}
require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space");
try { new AerialPlan24(List.of(new AerialPlan24.Site("balloon", AerialPlan24.Kind.BALLOON,
500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
catch (IllegalArgumentException expected) { }
verifyReservedShoreAndView(column);
System.out.println("Aerial24Smoke: nine deterministic plans, five sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
}
private static void verifyReservedShoreAndView(BlockState[] column) {
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst();
for (boolean obstructView : new boolean[]{false, true}) {
int cx = original.minX() + original.worldWidth() / 2;
int cz = original.minZ() + original.worldDepth() / 2;
int blockedX = obstructView ? (original.shoreX() + cx) / 2 : original.shoreX();
int blockedZ = obstructView ? (original.shoreZ() + cz) / 2 : original.shoreZ();
int[] refused = {0}, columns = {0};
var terrain = new AerialPlanner24.Terrain() {
public NoiseColumn column(int x, int z) { columns[0]++; return new NoiseColumn(0, column); }
public boolean reserved(int x, int z, int minY, int maxY) {
boolean blocked = Math.abs(x - blockedX) <= 8 && Math.abs(z - blockedZ) <= 8;
if (blocked) refused[0]++;
return blocked;
}
};
var replacement = AerialPlanner24.plan(0, 724, terrain);
require(refused[0] > 0 && !replacement.sites().getFirst().equals(original),
obstructView ? "A secret across the view segment rejects the original candidate" : "A secret over the shore rejects the original witness");
require(columns[0] <= AerialPlanner24.MAX_COLUMNS, "Reservations preserve the bounded terrain sampling budget");
for (var site : replacement.sites())
require(!terrain.reserved(site.shoreX(), site.shoreZ(), site.shoreY() - 1, site.shoreY() + 2),
"Every accepted shore stays outside planned root structures");
}
}
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
return new AerialPlan24.Site(site.id(), site.kind(), site.minX(), site.minY(), site.minZ(), site.width(), site.height(), site.depth(),
facing, site.seed(), site.shoreX(), site.shoreY(), site.shoreZ(), site.waterfall());
}
private static void verifyRender(AerialPlan24.Site site) {
Map<BlockPos, BlockState> complete = new HashMap<>(), clipped = new HashMap<>();
int[] chests = {0};
var writer = writer(site, complete, chests);
AerialRenderer24.render(site, writer);
require(chests[0] == 1 && complete.get(AerialGeometry24.chest(site)).is(Blocks.CHEST), "Each site has exactly one real chest");
int[] clippedChests = {0};
for (var chunk : site.bounds().intersectingChunks().toList())
AerialRenderer24.render(site, writer(site, clipped, clippedChests),
new BoundingBox(chunk.getMinBlockX(), 0, chunk.getMinBlockZ(), chunk.getMaxBlockX(), 383, chunk.getMaxBlockZ()));
require(complete.equals(clipped) && clippedChests[0] == 1, "Chunk clipping reconstructs the complete authored structure without duplicate chests");
long sources = complete.values().stream().filter(state -> state.is(Blocks.WATER)).count();
require(sources == (site.waterfall() ? 1 : 0), "Renderer writes one source, never a fabricated full waterfall column");
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var chest = AerialGeometry24.chest(site);
for (int dy = 1; dy <= 3; dy++) require(complete.get(chest.above(dy)) != null && complete.get(chest.above(dy)).isSolid(), "Mineral chest is buried under three intact layers");
require(complete.values().stream().anyMatch(state -> state.is(Blocks.COAL_ORE)), "Real mineable coal is available on the mineral islet");
require(complete.values().stream().anyMatch(state -> state.is(Blocks.IRON_ORE) || state.is(Blocks.DEEPSLATE_IRON_ORE)), "Real mineable iron is available");
int surface = 0, grass = 0;
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
int top = AerialGeometry24.terrainTop(site, x, z);
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
surface++;
if (complete.getOrDefault(AerialGeometry24.world(site, x, top, z), Blocks.AIR.defaultBlockState()).is(Blocks.GRASS_BLOCK)) grass++;
}
require(grass * 2 > surface, "Grass covers most intact surface columns while fissures retain exposed rock");
var trees = AerialGeometry24.trees(site);
require(!trees.isEmpty(), "Each mineral islet supports a small woodland");
for (var tree : trees) {
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
require(complete.get(ground).is(Blocks.GRASS_BLOCK), "Tree trunk is rooted in the real soil surface");
for (int y = 1; y <= tree.trunkHeight(); y++)
require(complete.get(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Tree trunk remains continuous");
require(complete.get(ground.above(tree.trunkHeight() + 1)).is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES),
"Tree crown fits entirely within its saved envelope");
}
for (var state : complete.values()) if (state.is(Blocks.OAK_LEAVES) || state.is(Blocks.BIRCH_LEAVES))
require(!state.getValue(BlockStateProperties.PERSISTENT) && state.getValue(BlockStateProperties.DISTANCE) < 7,
"Natural leaves have a log connection and may decay after players cut the tree");
}
}
private static LostCityRenderer.BlockWriter writer(AerialPlan24.Site site, Map<BlockPos, BlockState> blocks, int[] chests) {
return new LostCityRenderer.BlockWriter() {
public void set(int x, int y, int z, BlockState state) {
var pos = new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z);
require(site.bounds().isInside(pos), "Authored placement stays inside its saved native bounds"); blocks.put(pos, state);
}
public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
chests[0]++; require(loot.equals(AerialRenderer24.loot(site)), "Each chest uses its site's distinct loot family");
set(x, y, z, Blocks.CHEST.defaultBlockState());
}
};
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
@@ -0,0 +1,157 @@
package fr.koka.sanctuary.worldgen.aerial;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.expansion.ExpansionJournal;
import fr.koka.sanctuary.expansion.ExpansionPlacement;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Kind;
import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Real filesystem transaction checks; no Minecraft save, chunk or personal instance is opened. */
public final class AerialPersistence24Smoke {
private static final long SEED = Long.MIN_VALUE + 824;
private AerialPersistence24Smoke() {}
public static void main(String[] args) throws Exception {
Path scratch = Files.createTempDirectory("sanctuary-aerial-persistence24-");
try {
Path legacyDirectory = scratch.resolve("legacy23");
var legacy = new ExpansionJournal(legacyDirectory, SEED, 724, 23);
Path legacyFile = legacyDirectory.resolve(ExpansionJournal.FILE_NAME);
byte[] legacyBytes = Files.readAllBytes(legacyFile);
Path directory = scratch.resolve("sanctuary-world-v24");
Path storeDirectory = AerialStore24.directory(directory);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "New journal without an immutable plan");
require(!Files.exists(directory), "Rejected unbound journal must not leave an initialized directory");
var draws = new AtomicInteger();
var stored = AerialStore24.create(storeDirectory, SEED, 724, () -> { draws.incrementAndGet(); return fixture(); });
for (var site : stored.plan().sites()) {
if (site.kind() == Kind.MINERAL_ISLET) {
require(site.rockHeight() >= 20 && site.rockHeight() <= 35, "Vegetation does not reduce the rock thickness");
require(site.height() == site.rockHeight() + 8 && site.maxY() < 350, "The persisted envelope includes eight blocks of vegetation below the ceiling");
}
var expectedClaims = new java.util.HashSet<>(site.bounds().intersectingChunks().toList());
if (site.waterfall()) expectedClaims.addAll(site.waterfallClaimBounds().intersectingChunks().toList());
require(expectedClaims.equals(java.util.Set.copyOf(site.claimedChunks())), "Claims contain only body chunks and the local waterfall outlet");
for (var chunk : expectedClaims) require(site.ownsChunk(chunk.x(), chunk.z()), "Ownership agrees with the exact finite mask");
require(!site.ownsChunk((site.maxX() >> 4) + 2, (site.maxZ() >> 4) + 2), "Unused corners of the old broad margin stay free");
}
require(!Files.exists(directory), "Manifest creation must not prematurely initialize the journal");
require(draws.get() == 1 && stored.plan().sites().size() == 5, "One complete initial plan is recorded");
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24);
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
byte[] pristineJournal = Files.readAllBytes(journalFile);
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
"Only the new generation24 journal uses its new schema");
var reloaded = new ExpansionJournal(directory, SEED, 724, 24);
require(reloaded.aerialToken().equals(stored.token()) && reloaded.aerialPlan24().sites().equals(stored.plan().sites()),
"Reload obtains the exact stored sites and token without calling a planner");
require(draws.get() == 1, "Reload must not draw another layout");
require(reloaded.aerialPlan24().sites().stream().filter(s -> s.kind() == Kind.MINERAL_ISLET)
.map(Site::rockHeight).toList().equals(List.of(28, 32)),
"Reload preserves the original rock heights independently from vegetation headroom");
journal.commit(List.of(root));
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes");
var collision = child("collision", 672, 0);
require(!collision.conflicts(root), "Collision witness is outside initial island reservations");
require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks");
reject(() -> journal.commit(List.of(root, collision)), "Reservation over an unvisited aerial site");
require(journal.healthy() && Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)),
"Ordinary rejected geometry preserves the healthy journal");
var free = ExpansionPlacement.candidates(SEED, List.of(root), "free_space", "sanctuary", "south", "temperate", 64, "natural", 0)
.stream().filter(value -> !stored.plan().conflicts(value)).findFirst().orElseThrow();
journal.commit(List.of(root, free));
require(new ExpansionJournal(directory, SEED, 724, 24).islands().equals(List.of(root, free)),
"Claims do not immobilize the complete surrounding ring");
byte[] withFree = Files.readAllBytes(journalFile);
var poisoned = JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject();
poisoned.getAsJsonArray("islands").add(ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, collision).getOrThrow());
Files.writeString(journalFile, poisoned.toString());
byte[] poisonedBytes = Files.readAllBytes(journalFile);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Reload with an externally introduced claim overlap");
require(Arrays.equals(poisonedBytes, Files.readAllBytes(journalFile)), "Rejected reload does not repair or rewrite history");
Files.write(journalFile, withFree);
Path manifestFile = storeDirectory.resolve(stored.token() + ".json");
byte[] manifestBytes = Files.readAllBytes(manifestFile);
Files.writeString(manifestFile, "{}");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED, 724), "Changed manifest bytes");
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Changed manifest linked by an existing journal");
require(Arrays.equals(withFree, Files.readAllBytes(journalFile)), "Manifest corruption leaves the journal untouched");
Files.write(manifestFile, manifestBytes);
Files.delete(manifestFile);
reject(() -> new ExpansionJournal(directory, SEED, 724, 24), "Missing manifest cannot be regenerated on reload");
reject(() -> journal.commit(List.of(root, free.ready())), "Missing manifest stops later commits too");
require(!journal.healthy(), "Uncertain manifest authority stops the current session");
Files.write(manifestFile, manifestBytes);
require(new ExpansionJournal(directory, SEED, 724, 24).islands().equals(List.of(root, free)), "Explicit reload restores a repaired authority");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED + 1, 724), "Other world seed");
reject(() -> AerialStore24.load(storeDirectory, stored.token(), SEED, 1024), "Other initial size");
reject(() -> AerialStore24.load(storeDirectory, "../manifest", SEED, 724), "Non-digest token");
String duplicate = new String(manifestBytes, StandardCharsets.UTF_8).replaceFirst("\\{", "{\"schema\":1,");
String duplicateToken = digest(duplicate.getBytes(StandardCharsets.UTF_8));
Files.writeString(storeDirectory.resolve(duplicateToken + ".json"), duplicate);
reject(() -> AerialStore24.load(storeDirectory, duplicateToken, SEED, 724), "A correctly hashed manifest still rejects duplicate fields");
for (String option : List.of("global-off", "experimental-off")) {
Path offDirectory = scratch.resolve(option).resolve("sanctuary-world-v24");
var empty = AerialStore24.create(AerialStore24.directory(offDirectory), SEED, 724, () -> AerialPlan24.EMPTY);
var off = new ExpansionJournal(offDirectory, SEED, 724, 24, empty.token());
require(off.aerialPlan24().isEmpty() && off.aerialPlan24().claimsChunks().isEmpty(), "Disabled plan reserves no aerial sites");
require(new ExpansionJournal(offDirectory, SEED, 724, 24).aerialPlan24().isEmpty(), "Disabled plan stays disabled after reload");
off.commit(List.of(root, collision));
}
var alternative = AerialStore24.create(storeDirectory, SEED, 724, () -> AerialPlan24.EMPTY);
var swapped = JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject();
swapped.addProperty("aerial_plan", alternative.token());
var healthy = new ExpansionJournal(directory, SEED, 724, 24);
Files.writeString(journalFile, swapped.toString());
reject(() -> healthy.commit(List.of(root, free.ready())), "Another valid manifest cannot replace the current session's immutable layout");
Files.write(journalFile, withFree);
reject(() -> new ExpansionJournal(legacyDirectory, SEED, 724, 24), "No implicit migration from23");
reject(() -> new ExpansionJournal(directory, SEED, 724, 23), "No implicit downgrade to23");
legacy.commit(legacy.islands());
require(Arrays.equals(legacyBytes, Files.readAllBytes(legacyFile)), "Historical23 bytes remain identical");
require(!Files.readString(legacyFile).contains("aerial_plan") && !legacy.aerialPlan24().conflicts(collision), "No aerial authority added to23");
System.out.println("Aerial persistence24: manifest-first bootstrap, strict reload, claim conflicts, disabled plans and legacy23 passed");
} finally {
try (var paths = Files.walk(scratch)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.deleteIfExists(path);
}
}
}
private static ExpansionIsland child(String id, int x, int z) {
return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z,
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24);
}
private static AerialPlan24 fixture() {
return new AerialPlan24(List.of(
new Site("balloon", Kind.BALLOON, 660, 290, 0, 15, 28, 19, 0, 1, 390, 240, 0, false),
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 27, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 0, 3, -390, 240, 0, false),
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true),
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
}
private static String digest(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
private static void reject(IOAction action, String message) throws Exception {
try { action.run(); } catch (IOException expected) { return; }
throw new AssertionError("Expected rejection: " + message);
}
@FunctionalInterface private interface IOAction { void run() throws Exception; }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "0.1.0-alpha.23.1" version = "0.1.0-alpha.24"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]