feat(worldgen): add persistent aerial Sanctuary sites for alpha.24
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,6 +20,24 @@ publiée** : le placement des structures redevient sélectif. Les palettes,
|
||||
intérieurs et filons de l’alpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
|
||||
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 l’herbe, des fleurs et des petits arbres ; exactement
|
||||
l’un 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 l’option 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
|
||||
|
||||
Le [chantier de génération](docs/generation-alpha23.1.md) réunit le Blocodex, une
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
@@ -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 l’alpha.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 d’expansion,
|
||||
registre et admission d’au moins une structure adaptée par expansion acceptée.
|
||||
|
||||
Les cinq sites cherchent la périphérie, hors de l’emprise 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 n’est pas prolongé vers les sites.
|
||||
Aucun pont, quai, pilier jusqu’au sol ou accès automatique n’est 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 d’herbe 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 d’or, 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 d’eau 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 d’un 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 l’ensemble 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 s’applique qu’aux 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 d’eau, 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. L’admission
|
||||
d’expansion, le journal et la reprise de vide vérifient ces emprises même si
|
||||
aucun joueur n’a 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 qu’une famille de structures vanilla manque au serveur.
|
||||
`/locate structure` et `/sanctuary census structures aerial [site]` les
|
||||
référencent sans ouvrir d’expansion 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 n’est 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).
|
||||
@@ -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
|
||||
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
|
||||
```
|
||||
|
||||
L’accès Git est rétabli. Les artefacts sont prêts ; leur publication, l’avance
|
||||
du canal puis les deux synchronisations isolées et celles de l’instance
|
||||
Prism existante restent à effectuer. Le ZIP d’amorçage suit le canal stable ;
|
||||
il ne sert pas à réimporter l’instance existante.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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 l’ordre inverse
|
||||
de chargement. Les tests purs, deux admissions réelles d’expansion 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 n’est ouverte ou modifiée. Aucun essai Windows
|
||||
natif ni contrôle graphique du client avec shaders n’a é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 n’est retenu qu’aprè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 l’export 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 l’ordre 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 l’alpha.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.
|
||||
|
||||
L’ordre 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 s’ils 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 d’un 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 d’occupation 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 n’apparaî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 d’expansion reste inchangé pendant ces visites.
|
||||
|
||||
La source d’eau est présente et conservée ; l’essai 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 l’herbe**
|
||||
(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 l’emprise 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 d’adaptation 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 d’appels 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, l’unique
|
||||
source prévue produit de l’eau sous l’îlot à `399 194 312`, puis à
|
||||
`399 8 312`, après **1 180 ticks d’attente contrôlée**. À Y8, le fluide
|
||||
est de l’eau courante, pas une source artificiellement posée. Aucun chemin
|
||||
vertical de blocs sources n’est 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.
|
||||
L’exigence 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 l’ancienne limite de 200 ticks du fixture était trop courte.
|
||||
Le premier essai interrompu n’est 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ée–rivage |
|
||||
| --- | --- | --- | ---: |
|
||||
| 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 n’est 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 l’ajout de végétation aérienne, a été comparé à
|
||||
`build/alpha231-evidence/final-medium.json`. Tous les champs sont identiques,
|
||||
à l’exception 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.
|
||||
L’essai d’admission et d’eau utilise un nouveau monde :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryAerial24FlowAdmission=true \
|
||||
-PsanctuaryTestGeneration=24 -PsanctuaryTestDiameter=724 \
|
||||
-PsanctuaryTestSeed=0 -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
Cette dernière commande reproduit l’essai d’admission 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.
|
||||
|
||||
L’accè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
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.23.1
|
||||
pack_version=0.1.0-alpha.23.1
|
||||
mod_version=0.1.0-alpha.24
|
||||
pack_version=0.1.0-alpha.24
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -40,7 +40,11 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : '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.admission23', providers.gradleProperty('sanctuaryAdmission23Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.admission23.reserveOnly', providers.gradleProperty('sanctuaryAdmission23ReserveOnly').getOrElse('false'))
|
||||
@@ -94,6 +98,10 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('heritage21', heritage21)
|
||||
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
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 admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('admission23', admission23)
|
||||
@@ -111,7 +119,7 @@ tasks.named('processGametestResources') {
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: 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'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
@@ -588,3 +596,20 @@ tasks.named('check') { dependsOn('expansionOcean23Smoke') }
|
||||
}
|
||||
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') }
|
||||
|
||||
+179
@@ -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); }
|
||||
}
|
||||
}
|
||||
+398
@@ -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); }
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -61,12 +61,12 @@ public final class PopulationDiagnostics {
|
||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
|
||||
var expected = new java.util.HashSet<>(SanctuaryBiomeSource.REQUIRED_BIOMES);
|
||||
if (unified.source().citiesEnabled()) expected.add("sanctuary:lost_city");
|
||||
if (unified.rootGeneration() == 23) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
|
||||
if (unified.rootGeneration() == 23 || unified.rootGeneration() == 24) fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES
|
||||
.forEach(name -> expected.add("minecraft:" + name));
|
||||
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
|
||||
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName())
|
||||
.collect(java.util.stream.Collectors.toSet()).equals(expected),
|
||||
"Historical holders stay intact; 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(),
|
||||
"The unified starter must use its real terrain state, never the expansion host fallback");
|
||||
} else {
|
||||
@@ -215,7 +215,7 @@ public final class PopulationDiagnostics {
|
||||
|
||||
void prepare() throws IOException {
|
||||
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.
|
||||
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||
// Historical generators retain their original near-spawn contract.
|
||||
|
||||
+8
-5
@@ -61,7 +61,10 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
var historical22 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v22")));
|
||||
helper.assertTrue(historical22.value().createWorldDimensions().overworld() instanceof SanctuaryChunkGenerator old
|
||||
&& old.rootGeneration() == 22 && old.source().generation() == 22, "Historical22 remains separately readable");
|
||||
helper.assertTrue(generator.rootGeneration() == 23 && generator.source().generation() == 23 && generator.experimentalStructures(), "The public preset uses generation23 with its new-world option");
|
||||
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");
|
||||
verifyConstructionMasks(helper, (SanctuaryChunkGenerator) historical21.value().createWorldDimensions().overworld(), generator);
|
||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||
@@ -72,11 +75,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
|
||||
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();
|
||||
serialized.remove("experimental_structures");
|
||||
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(),
|
||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||
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.
|
||||
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)
|
||||
&& !active.intersectsExperimentalStructures(outside.getX(), outside.getY(), outside.getZ(),
|
||||
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()));
|
||||
for (var network : active.undergroundHall22().networks()) for (var module : network.modules())
|
||||
interior.add(new net.minecraft.core.BlockPos(module.centerX(), module.floorY(), module.centerZ()));
|
||||
if ((active.rootGeneration() == 22 || active.rootGeneration() == 23) && 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");
|
||||
for (var position : interior) helper.assertTrue(active.protectsCityDecoration(position)
|
||||
&& active.intersectsExperimentalStructures(position.getX(), position.getY(), position.getZ(),
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||
if (Boolean.getBoolean("sanctuary.test.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.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||
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;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 23);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 23 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 24);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 24 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
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.CHUNK_GENERATOR, id("island_v23"), SanctuaryChunkGenerator.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.transit.TransitStructures21.register();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
|
||||
|
||||
+2
-2
@@ -51,11 +51,11 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
contents.addChild(sizeButton);
|
||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
if (generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) {
|
||||
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)
|
||||
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||
(button, value) -> experimental = value));
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable(generator.rootGeneration() == 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));
|
||||
}
|
||||
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.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
|
||||
@@ -45,7 +45,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
public ExpansionIsland {
|
||||
if (!id.matches("[a-z][a-z0-9_]{0,31}") || !parent.matches("[a-z][a-z0-9_]{0,31}")) throw new IllegalArgumentException("Invalid island identifier");
|
||||
if (!DIRECTIONS.contains(direction) || !CLIMATES.contains(climate) || !reliefs(generation).contains(relief)) throw new IllegalArgumentException("Unknown island profile");
|
||||
if (relief.equals("ocean") && (generation != 23 || 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");
|
||||
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
|
||||
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
|
||||
@@ -59,7 +59,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
var priorities = new java.util.TreeSet<>(structurePriorities);
|
||||
if (priorities.size() != structurePriorities.size()) throw new IllegalArgumentException("Duplicate structure priority family");
|
||||
structurePriorities = List.copyOf(priorities);
|
||||
if (!structurePriorities.isEmpty() && (generation != 23 || id.equals("sanctuary")))
|
||||
if (!structurePriorities.isEmpty() && ((generation != 23 && generation != 24) || id.equals("sanctuary")))
|
||||
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);
|
||||
}
|
||||
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23; }
|
||||
public static List<String> reliefs(int generation) { return generation == 23 ? RELIEFS23 : RELIEFS; }
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24); }
|
||||
public static List<String> reliefs(int generation) { return (generation == 23 || generation == 24) ? RELIEFS23 : RELIEFS; }
|
||||
public boolean origin() { return id.equals("sanctuary"); }
|
||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.Strictness;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
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.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
@@ -41,6 +43,8 @@ public final class ExpansionJournal {
|
||||
private final int rootDiameter;
|
||||
private final int rootGeneration;
|
||||
private final DirectorySync directorySync;
|
||||
private String aerialToken = "";
|
||||
private AerialPlan24 aerialPlan24 = AerialPlan24.EMPTY;
|
||||
private List<ExpansionIsland> islands;
|
||||
private boolean failed;
|
||||
|
||||
@@ -52,7 +56,17 @@ public final class ExpansionJournal {
|
||||
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 {
|
||||
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 (rootDiameter != 512 && rootDiameter != 724 && !((rootGeneration == 13 || ExpansionIsland.spatialGeneration(rootGeneration)) && rootDiameter == 1024))
|
||||
throw new IOException("Unsupported expansion root diameter");
|
||||
@@ -62,10 +76,13 @@ public final class ExpansionJournal {
|
||||
this.file = this.directory.resolve(FILE_NAME);
|
||||
this.seed = seed;
|
||||
this.rootDiameter = rootDiameter;
|
||||
if (rootGeneration == 24 && !initialAerialToken.isEmpty()) bindAerial(initialAerialToken);
|
||||
else if (rootGeneration != 24 && !initialAerialToken.isEmpty()) throw new IOException("Historical journals cannot bind aerial sites");
|
||||
if (Files.exists(this.directory, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isDirectory(this.directory, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Expansion journal path is not a directory");
|
||||
this.islands = read();
|
||||
} else {
|
||||
if (rootGeneration == 24 && aerialToken.isEmpty()) throw new IOException("New generation24 journal requires its immutable aerial manifest");
|
||||
// Creating the directory is the bootstrap marker. An interrupted bootstrap requires repair.
|
||||
Files.createDirectories(this.directory.getParent());
|
||||
Files.createDirectory(this.directory);
|
||||
@@ -75,6 +92,8 @@ public final class ExpansionJournal {
|
||||
}
|
||||
|
||||
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. */
|
||||
public synchronized boolean healthy() { return !failed; }
|
||||
@@ -150,17 +169,20 @@ public final class ExpansionJournal {
|
||||
reader.setStrictness(Strictness.STRICT);
|
||||
JsonObject root = readJson(reader, 0).getAsJsonObject();
|
||||
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IOException("Trailing data in expansion journal");
|
||||
requireFields(root, JOURNAL_FIELDS);
|
||||
if (integer(root, "schema") != 1 || number(root, "world_seed").longValueExact() != seed || integer(root, "root_diameter") != rootDiameter) {
|
||||
var journalFields = new java.util.HashSet<>(JOURNAL_FIELDS);
|
||||
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");
|
||||
}
|
||||
if (rootGeneration == 24) bindAerial(string(root, "aerial_plan"));
|
||||
JsonArray entries = root.getAsJsonArray("islands");
|
||||
List<ExpansionIsland> decoded = new ArrayList<>();
|
||||
for (JsonElement element : entries) {
|
||||
JsonObject entry = element.getAsJsonObject();
|
||||
var fields = new java.util.HashSet<>(ISLAND_FIELDS);
|
||||
if (ExpansionIsland.spatialGeneration(rootGeneration) && entry.has("reuse_token")) fields.add("reuse_token");
|
||||
if (rootGeneration == 23 && entry.has("structure_priorities")) fields.add("structure_priorities");
|
||||
if ((rootGeneration == 23 || rootGeneration == 24) && entry.has("structure_priorities")) fields.add("structure_priorities");
|
||||
requireFields(entry, fields);
|
||||
decoded.add(new ExpansionIsland(string(entry, "id"), string(entry, "parent"), string(entry, "direction"), string(entry, "climate"),
|
||||
integer(entry, "diameter"), integer(entry, "center_x"), integer(entry, "center_z"), number(entry, "seed").longValueExact(),
|
||||
@@ -191,6 +213,7 @@ public final class ExpansionJournal {
|
||||
long dx = (long) island.centerX() - parent.centerX(), dz = (long) island.centerZ() - parent.centerZ();
|
||||
if (!(ExpansionIsland.spatialGeneration(island.generation()) ? ExpansionSpatialPlacement14.followsDirection(island.direction(), dx, dz) : followsDirection(island.direction(), dx, dz))) throw new IOException("Expansion position does not follow its direction: " + island.id());
|
||||
for (ExpansionIsland other : previous.values()) if (island.conflicts(other)) throw new IOException("Expansion reservations overlap: " + island.id() + " / " + other.id());
|
||||
if (rootGeneration == 24 && aerialPlan24.conflicts(island)) throw new IOException("Expansion reservation overlaps a permanent aerial site: " + island.id());
|
||||
previous.put(island.id(), island);
|
||||
}
|
||||
return checked;
|
||||
@@ -212,7 +235,8 @@ public final class ExpansionJournal {
|
||||
|
||||
private JsonObject encode(List<ExpansionIsland> entries) {
|
||||
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();
|
||||
for (ExpansionIsland island : entries) {
|
||||
JsonObject entry = new JsonObject();
|
||||
@@ -232,6 +256,14 @@ public final class ExpansionJournal {
|
||||
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 {
|
||||
if (depth > 4) throw new IOException("Expansion journal nesting exceeds its schema");
|
||||
return switch (reader.peek()) {
|
||||
|
||||
@@ -24,14 +24,14 @@ public final class ExpansionOcean23 {
|
||||
}
|
||||
private ExpansionOcean23() {}
|
||||
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) {
|
||||
return (int) Math.round(ExpansionRelief.worldY(island, 220));
|
||||
}
|
||||
/** Quick expansions retain their size/climate draw; only unspecified relief participates. */
|
||||
public static String quickRelief(long seed, String id, int diameter, String climate, int generation) {
|
||||
if (generation != 23 || diameter < 512 || climate.equals("lost_city")) return "natural";
|
||||
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";
|
||||
}
|
||||
public static Column column(ExpansionIsland island, int x, int z) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
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.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
@@ -37,7 +40,19 @@ public final class ExpansionRuntime {
|
||||
if (server.getWorldData().overworldData().isInitialized()
|
||||
&& !java.nio.file.Files.isDirectory(directory, java.nio.file.LinkOption.NOFOLLOW_LINKS))
|
||||
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());
|
||||
generator.initialize(level, journal.islands());
|
||||
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,
|
||||
int 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 {
|
||||
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");
|
||||
// Capture one immutable decision before any candidate is probed or certified. A resumed
|
||||
// reservation uses its recorded profile even if the world's census has since changed.
|
||||
var priorities = generator.rootGeneration() == 23
|
||||
var priorities = generator.rootGeneration() == 23 || generator.rootGeneration() == 24
|
||||
? fr.koka.sanctuary.knowledge.structures.StructureCensusService.priorities(level) : List.<String>of();
|
||||
var candidates = ExpansionPlacement.candidates(level.getSeed(), islands(), id, parent, direction, climate, diameter, relief, minimumDistance)
|
||||
.stream().map(value -> value.withStructurePriorities(priorities)).toList();
|
||||
.stream().filter(value -> !aerialConflicts(value)).map(value -> value.withStructurePriorities(priorities)).toList();
|
||||
boolean nativeRefused = false;
|
||||
for (var value : candidates) {
|
||||
ExpansionAdmission.checkDeadline(deadline);
|
||||
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;
|
||||
if (inspection == null) ExpansionAdmission.verify(level, value, deadline);
|
||||
if (probe(value) == null) throw new IllegalArgumentException("no_ground");
|
||||
@@ -143,6 +158,10 @@ public final class ExpansionRuntime {
|
||||
ExpansionAdmission.checkDeadline(deadline);
|
||||
throw new IllegalArgumentException(nativeRefused ? "native_structure_unavailable" : "no_free_space");
|
||||
}
|
||||
private boolean aerialConflicts(ExpansionIsland island) {
|
||||
return generator.rootGeneration() == 24 && generator instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& sanctuary.aerialPlan24().conflicts(island);
|
||||
}
|
||||
public void resume(String id) {
|
||||
requireHealthyJournal();
|
||||
var island = island(id);
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class ExpansionStructures {
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (generator.rootGeneration() == 23) {
|
||||
if ((generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) {
|
||||
ExpansionStructures23.generate(generator, registries, state, manager, chunk, templates, level);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ public final class ExpansionStructures23 {
|
||||
public static List<Entry> catalogue() { return ExpansionStructureCatalog23.entries(); }
|
||||
public static String family(String id) { return ExpansionStructureCatalog23.family(id); }
|
||||
public static boolean isPlacing() { return NativeStructurePlacement23.seaLevel() != null; }
|
||||
private static boolean nativeGeneration(int generation) { return generation == 23 || generation == 24; }
|
||||
public record Candidate(ChunkPos chunk, long roll) {}
|
||||
public record Accepted(Holder<Structure> structure, StructureStart start) {}
|
||||
public static final class NoSuitableStructureException extends IllegalArgumentException {
|
||||
@@ -90,7 +91,7 @@ public final class ExpansionStructures23 {
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
|
||||
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (island == null || island.generation() != 23) return;
|
||||
if (island == null || !nativeGeneration(island.generation())) return;
|
||||
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
|
||||
if (candidate == null) return;
|
||||
for (var old : chunk.getAllStarts().values()) {
|
||||
@@ -110,7 +111,7 @@ public final class ExpansionStructures23 {
|
||||
* No active source snapshot is mutated and no real world chunk is requested.
|
||||
*/
|
||||
public static void verifyExpansion(ServerLevel level, ExpansionIsland island, long deadlineNanos) {
|
||||
if (island.generation() != 23 || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
|
||||
if (!nativeGeneration(island.generation()) || island.origin() || !level.structureManager().shouldGenerateStructures()) return;
|
||||
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator host))
|
||||
throw new IllegalArgumentException("Native structure preflight requires a Sanctuary world");
|
||||
ExpansionBiomeSource isolated = isolatedSource(host, island, level.registryAccess());
|
||||
@@ -384,7 +385,7 @@ public final class ExpansionStructures23 {
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> wanted,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 23 || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
|| !nativeGeneration(generator.rootGeneration()) || !level.structureManager().shouldGenerateStructures()) return null;
|
||||
boolean nativeRequested = wanted.stream().anyMatch(h -> {
|
||||
var e = ExpansionStructureCatalog23.entry(h.unwrapKey().orElseThrow().identifier().toString());
|
||||
return e != null && e.availability() == Availability.OVERWORLD;
|
||||
@@ -392,7 +393,7 @@ public final class ExpansionStructures23 {
|
||||
boolean sanctuaryRequested = wanted.stream().anyMatch(h -> h.unwrapKey().orElseThrow().identifier().getNamespace().equals("sanctuary"));
|
||||
var candidates = new ArrayList<Candidate>();
|
||||
for (ExpansionIsland island : generator.source().islands()) {
|
||||
if (!nativeRequested || island.generation() != 23 || !island.status().equals("ready")) continue;
|
||||
if (!nativeRequested || !nativeGeneration(island.generation()) || !island.status().equals("ready")) continue;
|
||||
for (Candidate candidate : candidates(island)) {
|
||||
if (Math.abs((long) candidate.chunk().x() - (origin.getX() >> 4)) <= (long) radius * CELL_CHUNKS
|
||||
&& Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
|
||||
@@ -429,7 +430,7 @@ public final class ExpansionStructures23 {
|
||||
|
||||
private static void addSanctuaryCandidates(SanctuaryChunkGenerator generator, List<Candidate> positions) {
|
||||
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());
|
||||
}
|
||||
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()));
|
||||
var mineral = generator.mineralSanctuary23();
|
||||
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 BlockPos locator(StructureStart start, BlockPos origin) {
|
||||
@@ -536,7 +539,7 @@ public final class ExpansionStructures23 {
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
|
||||
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
|
||||
}
|
||||
@Override public int rootGeneration() { return 23; }
|
||||
@Override public int rootGeneration() { return island.generation(); }
|
||||
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
|
||||
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
|
||||
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
|
||||
|
||||
@@ -86,6 +86,9 @@ public final class ExpansionVoidReuse {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||
|| !ExpansionIsland.spatialGeneration(generator.rootGeneration()) || island.generation() != generator.rootGeneration() || island.origin())
|
||||
throw new IOException("Void reuse is restricted to a matching generation-14/15 expansion");
|
||||
if (generator.rootGeneration() == 24 && generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sanctuary
|
||||
&& sanctuary.aerialPlan24().conflicts(island))
|
||||
throw new ExpansionAdmission.OccupiedException("aerial_reserved: expansion overlaps a permanent initial site");
|
||||
List<ChunkPos> positions = positions(island);
|
||||
var entityStorage = entityRegion(level);
|
||||
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 {
|
||||
requireServerThread(level); close(level);
|
||||
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();
|
||||
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.
|
||||
|
||||
+23
@@ -16,6 +16,12 @@ public final class StructureCensusCommands {
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registries, environment) -> {
|
||||
var structures = Commands.literal("structures").executes(context -> report(context.getSource(), null))
|
||||
.then(Commands.literal("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())
|
||||
.suggests((context, builder) -> SharedSuggestionProvider.suggest(
|
||||
StructureCensusService.snapshot(context.getSource().getServer()).regions().stream()
|
||||
@@ -51,6 +57,23 @@ public final class StructureCensusCommands {
|
||||
send(source, "help");
|
||||
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) {
|
||||
var snapshot = StructureCensusService.snapshot(source.getServer());
|
||||
if (!snapshot.available()) { source.sendFailure(Component.translatable(TEXT + "unavailable")); return 0; }
|
||||
|
||||
+30
-2
@@ -83,7 +83,8 @@ public final class StructureCensusService {
|
||||
public static List<String> priorities(ServerLevel level) {
|
||||
assertServerThread(level.getServer());
|
||||
if (!level.dimension().equals(Level.OVERWORLD) || !level.structureManager().shouldGenerateStructures()
|
||||
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator) || generator.rootGeneration() != 23)
|
||||
|| !(level.getChunkSource().getGenerator() instanceof ExpansionChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 23 && generator.rootGeneration() != 24)
|
||||
return List.of();
|
||||
return confirmedPriorities(snapshot(level.getServer()), level.dimension().identifier().toString());
|
||||
}
|
||||
@@ -156,7 +157,7 @@ public final class StructureCensusService {
|
||||
}).toList();
|
||||
if (matching.isEmpty()) return Presence.OTHER_DIMENSION;
|
||||
// This is an explicit generation23 habitat requirement, not an inferred lack of biomes.
|
||||
if (island != null && island.generation() == 23 && !island.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;
|
||||
return null;
|
||||
}
|
||||
@@ -172,6 +173,31 @@ public final class StructureCensusService {
|
||||
.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,
|
||||
* rather than duplicated in the sidecar or inferred from currently loaded chunks. */
|
||||
public static List<ChunkKey> scope(String dimension, ExpansionIsland island) {
|
||||
@@ -222,6 +248,8 @@ public final class StructureCensusService {
|
||||
Session(StructureCensusStore store) { this.store = store; }
|
||||
}
|
||||
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 Region(String dimension, String island, int generation, String status, Coverage coverage, Map<String, FamilyCount> families) {
|
||||
public Region { families = Map.copyOf(families); }
|
||||
|
||||
+11
-2
@@ -20,6 +20,11 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
var region = (WorldGenRegion) (Object) this;
|
||||
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
|
||||
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
|
||||
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
|
||||
if (!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 owner = generator.islandForChunk(center.x(), center.z());
|
||||
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;
|
||||
// Vanilla rejects writes outside its region before reading the destination chunk.
|
||||
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;
|
||||
var island = generator.islandAt(pos.getX(), pos.getZ());
|
||||
if (fr.koka.sanctuary.expansion.ExpansionOcean23.active(island)
|
||||
@@ -55,7 +64,7 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
|
||||
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()
|
||||
&& generator.protectsCityDecoration(pos)) return;
|
||||
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)
|
||||
).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 PopulationIslandBiomeSource population;
|
||||
private final boolean cities;
|
||||
@@ -77,7 +81,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
this.generation = generation;
|
||||
this.cities = ExpansionIsland.spatialGeneration(generation);
|
||||
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,
|
||||
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
|
||||
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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
BiomeResolver woodlands = population.createResolver(sampler);
|
||||
|
||||
+80
-40
@@ -75,6 +75,41 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).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 boolean mineralPrepared23;
|
||||
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) {
|
||||
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;
|
||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||
}
|
||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 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) {
|
||||
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.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
|
||||
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)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 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);
|
||||
if (generation == 24) for (var site : aerialPlan24.sites()) {
|
||||
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|
||||
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
|
||||
}
|
||||
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public IslandCapacity capacity() {
|
||||
return switch (initialDiameter()) {
|
||||
@@ -185,7 +224,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
// Publish one root context before any generation workers use it.
|
||||
context(islands.getFirst());
|
||||
prepareCities(islands);
|
||||
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareMineral23(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
@@ -194,7 +233,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
prepareCities(islands);
|
||||
if (generation == 22 || generation == 23) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
if (generation == 22 || (generation == 23 || generation == 24)) { prepareTransit(islands); prepareUndergroundHall22(islands); }
|
||||
prepareMineral23(islands);
|
||||
prepareRuins(islands);
|
||||
prepareHeritage(islands);
|
||||
@@ -209,7 +248,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
return;
|
||||
}
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && !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;
|
||||
activeCityPlans = java.util.Map.of();
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
@@ -225,7 +264,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
cityProtection15 = protection.build();
|
||||
}
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) {
|
||||
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();
|
||||
for (var island : islands) {
|
||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||
@@ -240,13 +279,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(published);
|
||||
}
|
||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||
if ((generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) || !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 terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
var plan = generation == 23
|
||||
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.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
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);
|
||||
}
|
||||
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 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(),
|
||||
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
|
||||
var plan = generation == 23
|
||||
var plan = (generation == 23 || generation == 24)
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
: generation == 22
|
||||
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
|
||||
@@ -289,7 +328,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
(System.nanoTime() - start) / 1_000_000, plan.diagnostics());
|
||||
}
|
||||
private void prepareTransit(List<ExpansionIsland> islands) {
|
||||
if ((generation != 21 && generation != 22 && generation != 23) || generation == 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 terrain = context(root);
|
||||
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.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
int directColumns = 0;
|
||||
if ((generation == 22 || generation == 23) && transitPlan21.isEmpty()) {
|
||||
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
|
||||
directColumns = sampled.columnsUsed();
|
||||
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
@@ -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());
|
||||
}
|
||||
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())
|
||||
|| 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) {
|
||||
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();
|
||||
if (undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||
.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 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.LostCityUnderground22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), hallTerrain);
|
||||
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);
|
||||
}
|
||||
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 terrain = context(root);
|
||||
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) {
|
||||
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")))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityPlan.EMPTY;
|
||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||
long start = System.nanoTime();
|
||||
var terrain = context(island);
|
||||
if (generation == 21 || generation == 22 || generation == 23) {
|
||||
if (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = generation >= 22
|
||||
@@ -492,12 +531,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (generation == 23 && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 21 || generation == 22 || generation == 23) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 22 || generation == 23) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 24 && aerialPlan24.protects(pos)) return true;
|
||||
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
|
||||
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.protects(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| (generation == 22 || (generation == 23 || generation == 24)) && undergroundHallProtection22.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation != 14) return false;
|
||||
var island = islandAt(pos.getX(), pos.getZ());
|
||||
@@ -519,20 +559,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && 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) {
|
||||
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);
|
||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
}
|
||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) 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);
|
||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23) return null;
|
||||
if (generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && (generation != 23 && generation != 24)) return null;
|
||||
lostCityPlan(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(
|
||||
ServerLevel level, net.minecraft.core.HolderSet<net.minecraft.world.level.levelgen.structure.Structure> structures,
|
||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (generation == 23) 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))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if ((generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && 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);
|
||||
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);
|
||||
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 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) {
|
||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||
if ((generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || generation == 23) && 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));
|
||||
if (reserved != null) {
|
||||
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; }
|
||||
}
|
||||
+75
@@ -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");
|
||||
}
|
||||
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
|
||||
&& ((generator.rootGeneration() != 21 && generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || generator.cityPlacement21(island).core().surface().districts().contains(district)))
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24)) && generator.cityReservation16(island) != null
|
||||
&& ((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()));
|
||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) 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())
|
||||
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)));
|
||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
@@ -50,13 +50,13 @@ public final class LostCityStructure extends Structure {
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||
}
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) 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;
|
||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || generator.rootGeneration() == 23) ? 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)
|
||||
.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)));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ public final class LostCityUndergroundStructure22 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) 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());
|
||||
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()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
|
||||
+4
-4
@@ -41,9 +41,9 @@ public final class LostCityUndergroundStructures22 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) 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());
|
||||
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()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
@@ -63,14 +63,14 @@ public final class LostCityUndergroundStructures22 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) || !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);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<LostCityUnderground16.Network>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.undergroundHall22().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || (island.generation() != 22 && island.generation() != 23) || !island.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
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
|
||||
+5
-5
@@ -19,7 +19,7 @@ public final class HeritageCommands19 {
|
||||
var level = source.getLevel();
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 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"));
|
||||
return 0;
|
||||
}
|
||||
@@ -28,20 +28,20 @@ public final class HeritageCommands19 {
|
||||
for (var site : plan.sites()) source.sendSuccess(() -> Component.translatable("sanctuary.heritage.entry",
|
||||
Component.translatable("sanctuary.heritage.kind." + site.kind().name().toLowerCase(Locale.ROOT)),
|
||||
site.entry().x(), site.entry().y(), site.entry().z()), false);
|
||||
if (generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) 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;
|
||||
var at = fr.koka.sanctuary.worldgen.transit.TransitPiece21.locator(module);
|
||||
source.sendSuccess(() -> Component.translatable("sanctuary.transit.entry", at.getX(), at.getY(), at.getZ()), false);
|
||||
}
|
||||
if ((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) 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);
|
||||
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();
|
||||
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()
|
||||
+ generator.undergroundHall22().networks().size() : 0);
|
||||
}))));
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ public final class HeritageStructure19 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.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());
|
||||
if (island == null || !island.origin() || (island.generation() != generator.rootGeneration())) return Optional.empty();
|
||||
var plan = generator.heritagePlan19();
|
||||
@@ -23,9 +23,9 @@ public final class HeritageStructure19 extends Structure {
|
||||
// Local route segments render before buildings. No piece needs a distant link endpoint loaded.
|
||||
for (var segment : plan.segments())
|
||||
if (segment.startChunkX() == x && segment.startChunkZ() == z)
|
||||
pieces.add((generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? 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)
|
||||
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();
|
||||
for (var piece : pieces) {
|
||||
var box = piece.getBoundingBox();
|
||||
|
||||
+2
-2
@@ -57,7 +57,7 @@ public final class HeritageStructures19 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
@@ -79,7 +79,7 @@ public final class HeritageStructures19 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.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;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@ public final class MineralSanctuaryStructure23 extends Structure {
|
||||
public MineralSanctuaryStructure23(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 23 || !generator.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());
|
||||
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();
|
||||
if (plan.isEmpty()) return Optional.empty();
|
||||
var c = plan.chamber();
|
||||
|
||||
+4
-4
@@ -40,9 +40,9 @@ public final class MineralSanctuaryStructures23 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 23 || !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());
|
||||
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();
|
||||
if (plan.isEmpty() || plan.chamber().startChunkX() != chunk.getPos().x()
|
||||
|| plan.chamber().startChunkZ() != chunk.getPos().z()) return;
|
||||
@@ -63,14 +63,14 @@ public final class MineralSanctuaryStructures23 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 23 || !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);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var plan = generator.mineralSanctuary23();
|
||||
if (plan.isEmpty() || !generator.experimentalStructures()) return null;
|
||||
var c = plan.chamber();
|
||||
var island = generator.islandForChunk(c.startChunkX(), c.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != 23 || !island.status().equals("ready")) return null;
|
||||
if (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
|
||||
|| Math.abs((long) c.startChunkZ() - (origin.getZ() >> 4)) > radius) return null;
|
||||
var chunk = level.getChunk(c.startChunkX(), c.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
|
||||
@@ -13,7 +13,7 @@ public final class RuinsStructure17 extends Structure {
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.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());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return Optional.empty();
|
||||
var networks = generator.ruinsPlan17().networks().stream()
|
||||
@@ -30,9 +30,9 @@ public final class RuinsStructure17 extends Structure {
|
||||
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece((generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || (generator.rootGeneration() == 22 || generator.rootGeneration() == 23)) ? 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)
|
||||
.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; }
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ public final class RuinsStructures17 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||
|| !(generator 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;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != generator.rootGeneration()) return;
|
||||
@@ -78,7 +78,7 @@ public final class RuinsStructures17 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 17 && generator.rootGeneration() != 19 && generator.rootGeneration() != 20 && generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || !generator.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;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ public final class TransitStructure21 extends Structure {
|
||||
public TransitStructure21(StructureSettings settings) { super(settings); }
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 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());
|
||||
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());
|
||||
if (modules.isEmpty()) return Optional.empty();
|
||||
for (var module : modules) {
|
||||
@@ -25,7 +25,7 @@ public final class TransitStructure21 extends Structure {
|
||||
throw new IllegalStateException("Transit module exceeds local native reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
|
||||
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || generator.rootGeneration() == 23)
|
||||
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
|
||||
? new TransitPiece22(module) : new TransitPiece21(module)))));
|
||||
}
|
||||
|
||||
+4
-4
@@ -46,10 +46,10 @@ public final class TransitStructures21 {
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23))
|
||||
|| (generator.rootGeneration() != 22 && generator.rootGeneration() != 23) && !sanctuary.experimentalStructures()) return;
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)))
|
||||
|| (generator.rootGeneration() != 22 && (generator.rootGeneration() != 23 && generator.rootGeneration() != 24)) && !sanctuary.experimentalStructures()) return;
|
||||
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;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
StructureStart previous = manager.getStartForStructure(holder.value(), chunk);
|
||||
@@ -66,7 +66,7 @@ public final class TransitStructures21 {
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| (generator.rootGeneration() != 21 && (generator.rootGeneration() != 22 && generator.rootGeneration() != 23)) || (generator.rootGeneration() != 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;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
|
||||
@@ -225,5 +225,17 @@
|
||||
"sanctuary.structures.census.unknown_dimension": "Unknown dimension: %s",
|
||||
"sanctuary.structures.census.status.incompatible": "Incompatible habitat",
|
||||
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s"
|
||||
"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.status.incompatible": "Milieu incompatible",
|
||||
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
|
||||
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s"
|
||||
"sanctuary.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"
|
||||
}
|
||||
|
||||
+127
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+176
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+146
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+157
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "sanctuary:aerial_v24",
|
||||
"family": "balloon",
|
||||
"biomes": [
|
||||
"minecraft:the_void"
|
||||
],
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "sanctuary:aerial_v24",
|
||||
"family": "merchant_ship",
|
||||
"biomes": [
|
||||
"minecraft:the_void"
|
||||
],
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "sanctuary:aerial_v24",
|
||||
"family": "mineral_islet",
|
||||
"biomes": [
|
||||
"minecraft:the_void"
|
||||
],
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "sanctuary:island_v23",
|
||||
"type": "sanctuary:island_v24",
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
@@ -64,7 +64,7 @@
|
||||
"minecraft:snowy_beach",
|
||||
"minecraft:stony_shore"
|
||||
],
|
||||
"type": "sanctuary:island_v23"
|
||||
"type": "sanctuary:island_v24"
|
||||
},
|
||||
"initial_diameter": 724
|
||||
}
|
||||
|
||||
+94
@@ -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); }
|
||||
}
|
||||
+157
@@ -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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.23.1"
|
||||
version = "0.1.0-alpha.24"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user