Compare commits

...
Author SHA1 Message Date
koka bf327b75d4 Deliver alpha.30 plateaus, open river banks and underground routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 10:36:55 +02:00
koka 8c90c61c53 Checkpoint alpha29 terrain ledges, transit and native mineshaft corrections 2026-09-12 09:44:30 +02:00
koka 9f82577927 docs: record verified alpha28 publication and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 01:38:21 +02:00
koka 79d3185cba feat(worldgen): add volumetric caves and nearby aerial sites for alpha28
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 01:36:05 +02:00
koka ad234a808f docs: record verified alpha27 publication and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 00:37:49 +02:00
67 changed files with 3241 additions and 127 deletions
+23
View File
@@ -1,5 +1,28 @@
# Changelog
## 0.1.0-alpha.30 — 2026-09-12
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
à niveaux locaux et brèches de berge de cinq blocs maximum laissant couler leau.
Recherche des traversées mieux répartie, sanctuaire minéral avec seconde recherche
sur les flancs, mines vanilla mieux admises. Les navires conservent leur modèle
et leur placement proche du rivage. Nouveau monde de test nécessaire.
Voir [la génération](docs/generation-alpha30.md) et [les essais](docs/testing-alpha30.md).
## 0.1.0-alpha.29 — 2026-09-12
Petites corniches variables sur le relief 3D de la28 ; correction des traversées
et de ladmission des mines natives. Voir [le périmètre](docs/generation-alpha29.md)
et [les essais](docs/testing-alpha29.md). Nouveau monde de test nécessaire.
## 0.1.0-alpha.28 — 2026-09-12
Spéléologie par bruits natifs, déformations volumiques 3D et failles obliques bornées ;
bateaux rapprochés et îlots au-dessus de Sanctuary. Marais réduits. Mines,
huttes et donjons vanilla adaptés aux souterrains ; filons natifs selon la
profondeur dans la roche, dans l’île et les expansions. Nouveau monde de test.
Voir [génération](docs/generation-alpha28.md) et [validation](docs/testing-alpha28.md).
## 0.1.0-alpha.27 — 2026-09-12
Retour à la base alpha.24 après rejet visuel de lalpha.25. Le relief et les
+28 -2
View File
@@ -21,6 +21,32 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30 — plateaux, rivières ouvertes et traversées
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
reprennent leur place entre elles. Une rivière cherche un parcours à l’échelle
de l’île. Des brèches courtes dans ses berges et celles des bassins laissent
leau tomber sur les flancs ou dans le vide. Les traversées souterraines et
le placement du sanctuaire minéral sont renforcés ; les navires restaurés
restent proches des rivages.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.md),
[les essais et leurs limites](docs/testing-alpha30.md) et [packwiz](docs/packwiz.md).
Lalpha.29 reste un point de reprise local non publié, intégré à cette livraison.
## Alpha.28 — grottes et lieux proches
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
la roche, failles moins traversantes, reliefs déplacés et affaissés par du bruit 3D,
bateaux près des berges et deux îlots au-dessus de Sanctuary. Les marais
occupent moins de place. Mines, huttes et donjons vanilla bénéficient de
contrôles adaptés aux cavités ; les filons suivent la profondeur de la masse
rocheuse, y compris dans les expansions.
Créer un **nouveau monde**. Voir le [périmètre](docs/generation-alpha28.md),
les [résultats effectivement vérifiés](docs/testing-alpha28.md) et la
[distribution](docs/packwiz.md).
## Alpha.27 — retour au terrain de la 24
La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées
@@ -572,7 +598,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.23 |
| Sanctuary / pack | 0.1.0-alpha.29 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -601,7 +627,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.29.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+34
View File
@@ -966,3 +966,37 @@ marais volumétriques dans les cavités existantes et affiner les coques. Voir l
[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes
et les petites sorties deau seront traités un à un, avec retour en jeu entre
les changements ; ils ne sont pas déclarés implémentés dans cette passe.
### WG-30 — Spéléologie, reliefs et satellites proches — alpha.28
Branche `codex/caves-relief-alpha28`, depuis la 27 validée visuellement par le
créateur. Ajouter les bruits de grottes vanilla dans les masses rocheuses,
limiter et incliner les failles, développer des hauteurs avec ridge/erosion,
rapprocher les bateaux des vraies berges et placer les îlots au-dessus du terrain.
Moins de marais ; mines, huttes et donjons vanilla adaptés aux cavités réelles.
Les filons natifs doivent aussi alimenter les continents ouverts ensuite.
Aucun retour des salles ellipsoïdales ni des cônes de lalpha.25.
Résultats et limites : [génération](generation-alpha28.md), [essais](testing-alpha28.md).
### WG-31 — Corniches fines et traversées présentes — alpha.29
Branche `codex/terraces-transit-alpha29`, depuis lalpha.28 validée en jeu
sur Windows par le créateur. Reproduire la traversée manquante sur Grand,
graine `-7228211907433324401`, corriger ladmission et la pose des modules.
Ajouter un léger terracement variable au relief3D, préserver les profondeurs,
et vérifier les véritables mines vanilla sur la graine signalée.
Voir [génération](generation-alpha29.md) et [vérifications](testing-alpha29.md).
### WG-32 — Dernière passe de terrain alpha — alpha.30
Branche `codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61`. Concentrer la déformation signée dans trois zones, retrouver les
plateaux naturels entre elles et des rivières longues à niveaux locaux.
Ouvrir des brèches de berge courtes pour laisser leau couler librement.
Assurer une recherche mieux répartie des traversées, notamment en Grand ;
chercher aussi le sanctuaire minéral sur les flancs en cas de refus intérieur.
Conserver les bateaux proches, les cavités et les générateurs vanilla de la 28/29.
Voir [le périmètre](generation-alpha30.md) et [les preuves](testing-alpha30.md).
+61
View File
@@ -0,0 +1,61 @@
# Alpha.28 — roche à explorer
La 27 est la base validée par le créateur. Cette passe garde son île, les
secrets minéraux et les réseaux, puis ajoute du creusement dans la roche.
Les nouvelles cavités ne sont pas des salles de structure aux volumes prescrits.
## Terrain
Le champ commun compose les fonctions de bruit de Minecraft 26.3-pre-2 :
cheese et cave_layer, entrées/spaghetti 3D, spaghetti 2D et roughness, piliers
et noodle. Leur altitude est déplacée pour la masse flottante. Les coupes
restent soustractives et satténuent vers les limites verticales ; il ne
sagit pas de recopier toute la génération Overworld ou ses aquifères.
Les carvers historiques restent désactivés pour ne pas ajouter une seconde
passe étrangère aux colonnes de certification.
Le relief supérieur déforme le volume rocheux dans les trois axes avec les
bruits ridge/erosion natifs échantillonnés en 3D. Les déplacements sont signés :
soulèvements, affaissements et cisaillements varient aussi avec la profondeur.
Le déplacement est borné à ±16 blocs horizontalement et ±40 verticalement,
avec une transition progressive de Y180 à Y228. Lenveloppe de l’île reste
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
Ce nest pas une simulation physique de plaques tectoniques. Aucun cône ni
salle géométrique ne sert de forme à ajouter au terrain.
Larrivée et les côtes gardent leur place. Les anciennes failles deviennent
obliques, interrompues et limitées en hauteur, avec de la roche au-dessus et
en dessous. Les expansions utilisent la même source de terrain ; leur climat
et leur déformation propre sappliquent ensuite.
Le seuil du champ de marais passe de 0,04 à 0,25 : les anciens secteurs se
réduisent au profit des biomes de grottes précédents, dont les champignons.
La hauteur du monde, les nuages et les trois diamètres sont conservés.
## Exploration et ressources
Les bateaux vanilla restaurés recherchent désormais les vraies berges proches.
Les deux îlots sont au-dessus de Sanctuary, avec un espace libre sous leur
roche et un seul exutoire deau. Leurs corps et leurs chutes deau ont des
emprises distinctes ; rien ne réserve toute la colonne sous un îlot sec.
Les mines emploient les pièces vanilla, leurs supports, rails, coffres et
spawners. Une partie exposée peut passer si lensemble reste ancré. Les huttes
cherchent aussi des berges souterraines de marais : sol local, couverture de
la cavité et quatre pilotis natifs. Les salles à spawner sont de vraies
MonsterRoomFeature, posées sur des candidats réels et soumises aux refus
natifs. Leur présence nest pas garantie sur chaque île.
Une passe de filons native complète les distributions existantes dans les
chunks de l’île et des expansions. Charbon, fer, cuivre, or, lapis, redstone,
diamant et émeraude utilisent les formes OreFeature vanilla. La profondeur
se mesure dans lenveloppe rocheuse entière de la colonne, sans recommencer
le calcul à chaque plafond de grotte. Les stocks restent finis. Les minerais
du Nether restent dans le Nether ; le Deep Dark nest pas ajouté à l’île.
## Livraison de test
Mod et pack `0.1.0-alpha.28`, Minecraft `26.3-pre-2`, codec de prototype
`sanctuary:island_v24` conservé. Créer un nouveau monde. Aucune migration,
aucune régénération ni intervention dans les sauvegardes personnelles.
Lalpha.27 et ses artefacts restent disponibles. Les vérifications réelles
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
+56
View File
@@ -0,0 +1,56 @@
# Alpha.29 — corniches fines et traversées présentes
La 28 a été validée visuellement par le créateur sur Windows, avec un chargement
correct. Cette livraison conserve cette base et traite trois observations :
la grande traversée absente en Grand, le relief un peu trop lissé et les mines
vanilla difficiles à rencontrer.
## Témoin signalé
Grand **1 024**, graine **-7228211907433324401**. La reproduction avec le code
28 confirme un plan de transit vide : 419 alignements examinés, 13 000 colonnes,
75,114 secondes de recherche et aucun couple de portiques naturels accepté.
Dans les douze premiers candidats natifs examinés, aucune mine nest admise.
Le résultat ne prétend pas inventorier les parties inconnues de toute l’île.
## Relief
La déformation 3D de la 28 reste en place. Une modulation fine du Y échantillonné
forme de petites corniches avec des ruptures plus franches. Période locale de
6 à 9 blocs et phase variables avec les bruits 3D existants, décalage supplémentaire
borné à ±2,5 blocs. Aucune grille de plateaux horizontaux commune à toute l’île.
Le raccord commence à Y214 et atteint sa pleine amplitude à Y244, à l’écart
de larrivée et du contour. Les densités sous Y214 sont exactement celles de
la 28 ; les grottes, failles, minerais, marais et lieux aériens sont conservés.
Le même champ sert aux colonnes, chunks, biomes et certifications.
## Infrastructures et mines
Le correctif de traversée conserve le génie civil et les galeries industrielles
existants. Les accès doivent sadapter au relief sans transformer la surface en
une grande fondation. Deux accès relient une chaussée et ses trottoirs ; la
branche vers une salle conserve une paroi pleine du côté fermé de sa jonction.
Les modules déjà certifiés ne sont plus refiltrés par un
biome souterrain incompatible avec leur ancien tag de structure.
Pour les mines, les protections sont vérifiées autour des vraies pièces et de
leurs appuis, et non dans tout le rectangle vide entre leurs branches. Le
graphe, les pièces, les rails, coffres, toiles et spawners restent vanilla.
Un sol rocheux portant déjà la galerie est reconnu comme appui natif ; il ne
nécessite pas un second pilier ou une chaîne. Les galeries exposées gardent les
ancrages bornés du vanilla et les réservations protègent aussi leurs appuis.
Les trois essais daltitude sont choisis dans les couches rocheuses réellement
présentes autour des pièces, après un sondage de 32 colonnes au maximum. Les
croisements utilisent le contrat de leur plancher vanilla : une voûte rocheuse
proche nest pas nécessaire à un pont de planches portant sur un corridor ancré.
Les marches creusées et les salles gardent leurs contrôles de couverture.
Les mines restent conditionnelles : le correctif nimpose pas une mine à chaque
graine et ne modifie pas la politique des autres familles de structures.
## Distribution
Mod et pack **0.1.0-alpha.29**, Minecraft **26.3-pre-2**, codec de prototype
`sanctuary:island_v24`, mêmes tailles et bouton Personnaliser. Les options de
structures gardent leur autorité. Créer un nouveau monde ; aucune modification
ou régénération dune sauvegarde personnelle. Les artefacts28 restent accessibles.
Les constats mesurés et les limites sont consignés dans [les essais](testing-alpha29.md).
+80
View File
@@ -0,0 +1,80 @@
# Alpha.30 — plateaux, rivières et traversées
Dernière passe de génération alpha demandée par le créateur. Branche
`codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61` ; cette dernière na pas été publiée. Minecraft **26.3-pre-2**,
Fabric Loader **0.19.5**, Fabric API **0.160.0+26.3** conservés.
## Relief
La déformation signée par les bruits Minecraft de la 28 agit dans trois zones
étendues, orientées par la graine. Entre elles, son poids devient nul et le
champ de plateaux naturel précédent reprend sa place. Il sagit de déplacer
et daffaisser la roche existante, pas dajouter trois cônes. Le petit
terracement systématique essayé en 29 est désactivé.
Les cavités et failles bornées de la 28 restent actives. Aucun nouveau
creusement ellipsoïdal, plafond du monde ou changement des nuages. La variation
de relief est nulle sous Y180. Les tailles 512 / 724 / 1 024 et le bouton natif
Personnaliser sont conservés, avec Moyen par défaut.
Ce changement vise des étendues plus calmes séparées par du relief marqué.
Il ne garantit pas que toute surface ait exactement deux niveaux, ni que toute
petite pointe rocheuse ait disparu. Le rendu est à juger dans un nouveau monde.
## Hydrologie
Une recherche sur la principale masse connectée cherche une rivière longue,
évitant les fortes pentes quand un passage plus bas existe. Elle suit le terrain
réel avec un lit de cinq blocs environ, des niveaux locaux, du gravier et de
largile. Les petits bassins et réseaux précédents restent présents.
Quand une berge est séparée dun espace ouvert par **cinq blocs au maximum**,
une brèche de trois blocs de largeur ouvre le passage. Le plan cherche jusqu’à
six sorties espacées pour la grande rivière et une sortie supplémentaire par
réseau régional. Il nimpose pas deux cascades terminales, ni de bassin de
réception : leau peut descendre sur l’île ou dans le vide. La brèche ne pose
ni fondation ni colonne deau artificielle ; les ticks vanilla propagent leau
depuis les sources voisines. Les emprises de chute restent réservées aux
expansions. Larrivée naturelle reste à l’écart du tracé.
La recherche travaille sur une grille puis vérifie les blocs du lit. Les
passages trop creux peuvent interrompre ce lit et laisser fuir leau. Ce nest
pas une simulation de bassin versant avec débit conservé ou pente monotone.
Ces ajouts concernent l’île initiale ; les profils des continents dexpansion
ne sont pas remplacés par cette rivière.
## Traversée et sanctuaire minéral
La recherche des deux entrées répartit plus tôt ses essais entre plusieurs
alignements au lieu d’épuiser une seule bande. Les appuis et la protection
des secrets gardent leur autorité. La seconde passe peut omettre un pied
optionnel protégé seulement si les autres appuis restent valides. Les modules
industriels, escaliers, trottoirs et parois existants sont conservés.
Le sanctuaire garde ses 79 × 73 × 27 blocs et sa palette minérale. Après les
48 candidats intérieurs, une seconde recherche de 48 candidats sur les flancs
accepte davantage dexposition, avec des attaches rocheuses et le plancher
praticable. Un échec complet reste diagnostiqué ; la structure ne peut pas être
posée dans un volume intégralement vide ou en écrasant un autre secret.
Aucun boss, machine, portail actif ou dimension ajouté.
Les corrections de mines de la 29 sont intégrées : hauteur choisie dans la
roche disponible, admission des pièces et appuis réels, croisements en bois
acceptés lorsquils sont portés. Les pièces, spawners, rails et butins restent
vanilla. Leur présence reste conditionnelle aux candidats et au terrain.
## Satellites et sauvegardes
Deux navires restaurés près des rivages et deux îlots au-dessus de Sanctuary,
dont un à cascade : modèles et règles de proximité conservés. Les points de
vue doivent désormais éviter les sommets que lhydrologie va creuser en bassin
ou en brèche ; aucun bloc artificiel nest ajouté pour soutenir un témoin.
Le prototype conserve `sanctuary:island_v24` et ses profils courants, conformément
au choix de travailler sur des mondes de test jetables. **Créer un nouveau monde.**
Il ny a ni migration ni régénération dune sauvegarde personnelle. Les réglages
Générer les structures et Structures expérimentales gardent leur rôle ; la
rivière et le relief sont des phénomènes naturels indépendants de ces options.
Les résultats effectivement mesurés sont dans [les vérifications](testing-alpha30.md).
+46 -4
View File
@@ -13,7 +13,37 @@ 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.27 prête à publier — 12 septembre 2026
## Alpha.28 publiée — 12 septembre 2026
La [release alpha.28](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.28)
est publiée depuis `79d3185`. Elle prolonge la 27 avec les cavités natives,
la déformation 3D signée du relief, des failles bornées, les mines/donjons et
les filons, moins de marais, deux bateaux près des berges et deux îlots au-dessus
de Sanctuary. Les huttes restent conditionnelles. Voir
[la génération](generation-alpha28.md) et [les résultats](testing-alpha28.md).
`check build` passe sur Mac : **21/21 GameTests natifs**. `assemblePack`,
JAR, MRpack et ZIP damorçage sont vérifiés localement puis par téléchargement
public. Minecraft 26.3-pre-2 et les dépendances restent inchangés.
Le [MRpack autonome](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.28/Sanctuary-0.1.0-alpha.28.mrpack)
est disponible. Le canal packwiz est au commit
`6ec1c10c710870a2ccc591b524cc26d125ca03a2`. Deux synchronisations isolées,
puis deux dans linstance existante **Sanctuary Beta**, réussissent.
Un seul JAR Sanctuary alpha.28 est actif ; les **925 fichiers personnels
et réglages suivis** gardent leurs empreintes. Le jeu était fermé.
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.28/`.
SHA-256 JAR : `7f95e9ebd50063c7ebfc649fa325e75263649e42c02e7159244953e09e27d24e`.
SHA-256 MRpack : `12d4db8f06a13fb19feb08e61325361f7ab4d783d6f43b3d0c3582d316c54d0f`.
Reçus : `build/alpha28-delivery-public.json`,
`build/alpha28-isolated-validation.json`, `build/alpha28-prism-validation.json`.
Créer un **nouveau monde Sanctuary** ; témoin **Moyen 724, graine 0**.
Les tests natifs concernent Mac, pas Windows. Le rendu client et les surplombs
restent à juger en jeu. Aucun monde personnel na été régénéré.
## Alpha.27 publiée — 12 septembre 2026
La reprise repart directement de lalpha.24 : relief et cavités conservés,
marais souterrains localisés, montgolfière retirée et navires fondés sur le
@@ -22,9 +52,21 @@ Voir [le périmètre](generation-alpha27.md) et [les essais](testing-alpha27.md)
Minecraft 26.3-pre-2 et les dépendances de la 24 sont conservés.
`check build` puis `assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP
damorçage sont vérifiés localement. La publication et la synchronisation de
linstance existante sont les prochaines étapes ; elles ne sont pas encore
attestées par ce commit.
damorçage sont vérifiés localement puis par téléchargement public.
La [release alpha.27](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.27)
est publiée depuis `392ade0`. Le canal packwiz est au commit
`9803b2279aa5242c9e3e7202b940bf47e1c3f404`. Le
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.27/Sanctuary-0.1.0-alpha.27.mrpack)
est disponible séparément.
Deux synchronisations isolées, puis deux synchronisations dans la même instance
Prism **Sanctuary Beta** réussissent. Un seul JAR Sanctuary alpha.27 est actif.
Les **925 fichiers personnels et réglages suivis** gardent leurs empreintes ;
le jeu était fermé. Le dossier reste `Sanctuary-0.1.0-alpha.1`, avec sauvegarde
ciblée dans `sanctuary-backups/before-0.1.0-alpha.27/`.
Reçus locaux : `build/alpha27-delivery-public.json`,
`build/alpha27-isolated-validation.json`, `build/alpha27-prism-validation.json`.
SHA-256 JAR : `56144b291407650e148aa3769596165a5e87398ea4a847cf67352ce2a2c625bf`.
SHA-256 MRpack : `e13be90c6454bad3cdd3418fc54b827488700876f6e4ddb42e4a05f57acfecb6`.
+8
View File
@@ -95,3 +95,11 @@ Aucun essai Windows natif, mesure de FPS ou validation visuelle par le joueur
n'est revendiqué. La durée du build comprend les vérifications et ne mesure
pas le temps de chargement d'une partie. La publication des artefacts et la
synchronisation Prism font l'objet de reçus de livraison distincts.
## Livraison
`assemblePack` réussit. JAR, MRpack, dépendances et ZIP damorçage vérifiés,
puis téléchargement anonyme des mêmes artefacts. Deux installations isolées
et deux synchronisations de linstance Prism existante réussissent ; les
925 fichiers personnels et réglages suivis sont inchangés. Voir
[la livraison](packwiz.md#alpha27-publiée--12-septembre-2026).
+110
View File
@@ -0,0 +1,110 @@
# Alpha.28 — vérifications
## Protocole
Tests sur macOS avec Java 25 et Minecraft 26.3-pre-2. Aucun essai Windows
natif ni jugement de rendu client nest revendiqué. Le créateur a validé
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
nouvelle passe, sans représenter leau, la végétation ou les structures.
Commande de livraison :
```sh
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true
```
`sanctuaryDensityMaps=false` coupe la production des anciennes images,
pas les assertions. Le monde GameTest est recréé dans le dossier de développement
ignoré. Les sauvegardes personnelles ne sont pas utilisées par les tests.
Les smokes de placement aérien examinent 512 / 724 / 1 024, graines 0 / 42 /
2026. Le témoin natif chargé en chunks FULL est Moyen 724, graine 0 ; ceci
ne prétend pas être neuf îles entièrement explorées. Les comparaisons de
terrain analysent deux coupes espacées de 2 blocs et une grille de hauteurs
espacée de 32 blocs. Une expansion naturelle de 512 est contrôlée par son
vrai champ de densité sans être réservée ni ouverte dans le serveur.
## Résultats
`check build` passe en **4 min 15 s**, dont **21/21 GameTests natifs**
en 57,06 s. `assemblePack` passe ensuite sans recompilation des binaires vérifiés.
Les smokes couvrent aussi le Blocodex, les formats de journal, les priorités
et les réservations dexpansion, ainsi que les anciennes géométries pures.
Le terrain de référence conserve **6 098 des 6 860** anciens échantillons
rocheux intérieurs (88,9 %), avec **762** désormais creusés. Cette comparaison
inclut les raccords du nouveau cisaillement, pas seulement les grottes.
Lexpansion de 512 conserve 1 299 échantillons contre 136 creusés.
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 sabaissent.
La hausse maximale relevée est de 32 blocs et le sommet échantillonné atteint
Y280. La baisse maximale de sommet, 132 blocs en (0,160), correspond à un
lobe supérieur qui quitte cette colonne : le sol observé passe de Y248 à
Y116. Elle ne désigne pas un déplacement vertical de 132 blocs ; le déplacement
3D est borné à ±40 en Y. Des surplombs et étranglements subsistent et restent
à apprécier visuellement dans le jeu.
Les **9 chunks FULL** inspectés contiennent les huit minerais Overworld :
| Minerai | Blocs observés, variantes deepslate comprises |
|---|---:|
| Charbon | 1408 |
| Fer | 325 |
| Cuivre | 302 |
| Or | 87 |
| Lapis | 53 |
| Redstone | 77 |
| Diamant | 16 |
| Émeraude | 4 |
Il sagit dun relevé après génération, pas de quotas imposés à chaque chunk.
La relecture finale des fichiers de région confirme **trois donjons vanilla**
avec spawners zombies et coffres `minecraft:chests/simple_dungeon` :
`-101 170 100`, `-92 175 -92` et `-85 178 11`. Une vraie mine est présente
vers `152 176 -120`, avec **143 pièces sauvegardées**. Dans les 14 chunks FULL
du contrôle des structures : 91 planches, 88 barrières, 13 rails, 29 toiles
et un spawner ont été observés dans les pièces chargées. Ce relevé sur les
fichiers MCA après fermeture du serveur ne génère aucun chunk supplémentaire.
Les quatre lieux aériens ont de vrais départs et pièces sauvegardées,
quatre coffres et deux marchands adultes au total, puis passent les contrôles
après ouverture, extraction et modification dans le protocole natif. Il y a
exactement une source de cascade et deux îlots survolant le terrain. Aucun
nouveau site ni habitant nest recréé pour remplacer ce qui a été utilisé.
Preuves locales ignorées : `build/alpha28-check-build-final.log`,
`build/alpha28-preview/geology-final.json`,
`build/alpha28-preview/geology-final-comparison.png`,
`build/alpha28-evidence/final/` et reçus de distribution `build/alpha28-*`.
## Limites et essai du créateur
- La hutte de sorcière est conditionnelle aux berges 3D, à lespace et aux quatre
appuis natifs. Le premier petit échantillon nen contient aucune : son audit
de placement ne constitue pas une preuve de présence en jeu.
- Les quatre départs et les deux UUID de marchands persistants sont relus
dans les données sauvegardées. Aucun second lancement JVM avec ce monde
alpha.28 nest revendiqué.
- La source de la cascade est contrôlée ; son écoulement complet jusquau vide
nest pas mesuré par ce protocole. La propagation reste celle de Minecraft.
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
lancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
que le nouveau relief serait identique à celui de la 27.
- Il ne sagit pas dun benchmark client ni dune simulation physique tectonique.
Les performances pourront être étudiées après ce retour visuel.
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0** pour
retrouver le témoin. Garder ses autres sauvegardes ; aucune migration ni
régénération nest réalisée. Valider en priorité les masses soulevées et
abaissées, les cavités à explorer, les îlots au-dessus du terrain et les
navires proches des berges.
## Distribution
Le JAR, le MRpack et le ZIP damorçage ont les mêmes empreintes sur disque
et après téléchargement anonyme de la release. Les deux synchronisations
isolées sont identiques ; les deux synchronisations de linstance Prism
existante conservent les empreintes des 925 fichiers personnels et réglages
suivis. Un seul JAR alpha.28 est actif. Voir [le reçu de livraison](packwiz.md).
+109
View File
@@ -0,0 +1,109 @@
# Alpha.29 — vérifications
**Point de reprise local, non publié.** Le chantier se poursuit directement
en alpha.30 à la demande du créateur. Le contrôle supplémentaire Petit512,
graine2026, a refusé la traversée et révélé un témoin côtier aérien sans appui
après génération. Ces limites empêchent de considérer la29 comme livrée.
Les artefacts préparatoires et reçus locaux restent des preuves intermédiaires.
État des preuves au 12 septembre 2026. Les essais natifs ci-dessous ont été
exécutés sur **Mac, en serveur de développement sans rendu client**. Le retour
Windows du créateur concerne lalpha.28 ; aucun essai Windows de la 29 nest
revendiqué. Créer un **nouveau monde** pour tester cette génération.
## Traversées réellement générées et parcourues
| Monde | Départs / chunks FULL | Distance entre accès | Parcours avec collisions natives |
|---|---:|---:|---:|
| Grand 1 024, graine **-7228211907433324401** | 23 / 52 | 340 blocs | 652 pas, 74 points accessibles |
| Moyen 724, graine **0** | 17 / 27 | 238 blocs | 390 pas, 56 points accessibles |
Chaque témoin vérifie les blocs posés, les escaliers, les approches naturelles,
les raccords entre modules, les départs sauvegardables et le locator dentrée.
Les **cinq cellules du côté fermé de la jonction en T** sont contrôlées après
la correction du renderer. Ces recherches nouvrent aucune expansion.
Preuves : `build/alpha29-evidence/user-locate-before-crossing/transit29-native.json`
(PID 55232) et `build/alpha29-evidence/medium-0/` (reçus du PID 56574).
La correction ultérieure de ladmission des mines ne modifie pas le transit.
Sur la même graine Grand, la référence alpha.28 produisait un plan vide après
419 alignements et 13 000 colonnes. Cette reproduction est conservée dans
`build/alpha29-evidence/baseline-user/` ; le succès de ses anciens GameTests
ne démontrait pas la présence dune traversée.
## Relief : comparaison exacte avec la 28
| Monde | Densités inférieures identiques bit à bit | Échantillons supérieurs ayant changé de signe |
|---|---:|---:|
| Grand, graine signalée | 10 140 | 303 / 71 656, soit **0,423 %** |
| Moyen, graine 0 | 7 935 | 166 / 56 074, soit **0,296 %** |
Les densités inférieures sont échantillonnées entre Y32 et Y214. Les deux
coupes à Z0 et Z96, au pas dun bloc dans la bande Y205305, ont été comparées
pour la graine signalée : petites corniches, quelques arêtes plus dentelées,
grandes silhouettes de la 28 conservées. Cela ne remplace pas un essai visuel
dans le client. Le déplacement supplémentaire des coordonnées est borné à
2,5 blocs ; cette borne ne porte pas sur le changement de sommet lorsquune
petite lèvre disparaît.
Reçus `alpha29-terraces-*.json` des dossiers de preuve ; aperçu local :
`build/alpha29-preview/terraces-1024--7228211907433324401.png`.
## Mine vanilla et satellites
**Moyen, graine 0 :** une véritable mine vanilla de **143 pièces acceptées**
est trouvée parmi les candidats normaux. Linspection est partielle :
**16 chunks FULL** sont demandés par le test. Elle observe notamment
244 planches de chêne, 200 barrières, 102 rails et 50 toiles dans les pièces
chargées. Une salle `monster_room` avec spawner de zombies est également
observée. Le test ne prétend pas avoir exploré les 143 pièces, ni avoir
déclenché tous les spawners ou contrôlé tous les butins. La hutte de sorcière
est absente de cet échantillon ; ce nest pas une absence prouvée sur l’île.
Une lecture séparée du monde sauvegardé confirme deux spawners vanilla
`cave_spider` aux positions **156 171 -138** et **140 175 -97**, dans les
corridors correspondants (`sc=1`, `hps=1`). Reçu :
`build/alpha29-evidence/medium-0/mineshaft-spawners-saved.json`.
Sur ce même monde, **quatre satellites**, **deux villageois**, **quatre coffres**
et **une source de cascade** passent la création puis un véritable redémarrage.
Les 47 chunks FULL contrôlés conservent le pillage, les villageois déplacés
et les blocs détruits, sans duplication ni reconstruction des témoins.
Le journal dexpansion reste inchangé. Création PID 56060, rechargement
PID 56574 ; les **23 GameTests natifs** de ce dernier processus passent.
Preuves regroupées dans `build/alpha29-evidence/medium-0/`.
## Tests ciblés et portée
Les smokes terrain, appuis de mines et transit couvrent les bornes, les
orientations et les refus protégés. Le test pur de traversée couvre
0 / 42 / 2026 × 512 / 724 / 1 024 **sur un terrain synthétique portant** :
ce nest pas une matrice de neuf mondes Minecraft réellement générés.
Les contrôles natifs ci-dessus portent sur les témoins explicitement cités.
`./gradlew check build` passe : **86 tâches**, dont les **23 GameTests natifs**,
en 2 min 58 s pour cette exécution. Journal :
`build/alpha29-evidence/medium-0/gradle.log`. Ce temps nest pas un benchmark de génération.
`./gradlew assemblePack` passe également. JAR, MRpack autonome, index packwiz
et ZIP damorçage sont vérifiés localement : versions, dépendances, CRC et
empreintes concordent. Reçus `build/alpha29-artifacts.json` et
`build/alpha29-delivery-local.json`.
Restent en attente de leurs reçus :
- Contrôles natifs de **planification seule** Grand/graine42 et Petit/graine2026 ;
même réussis, ils ne prouveront pas un parcours sur des chunks FULL.
- Publication, canal packwiz et synchronisation Prism.
Pas de preuve dexécution avec structures désactivées propre à lalpha.29,
ni de nouvelle validation visuelle client ou Windows à ce stade.
## Repères pour un essai manuel
Avec les structures activées, sur un nouveau Grand de graine
`-7228211907433324401`, les entrées parcourues sont **128 252 -127** et
**128 257 213**. Sur Moyen/graine0 : **-90 235 -127** et **-90 235 111**.
`/locate structure sanctuary:industrial_transit` recherche une entrée du réseau.
Le repère **152 175 -120** désigne la mine de Moyen/graine0 : lutiliser en
spectateur pour linspection, sans supposer une arrivée sûre en survie.
+145
View File
@@ -0,0 +1,145 @@
# Alpha.30 — vérifications
Essais du 12 septembre 2026 sur **Mac**, dans des serveurs de développement
sans rendu client. Le retour Windows du créateur concerne la 28 ; aucun essai
Windows natif de la 30 nest revendiqué. Nouveau monde nécessaire.
## Témoin Grand signalé par le créateur
**1 024 blocs, graine `-7228211907433324401`**, processus 62002 :
**24/24 GameTests passent**. Reçus dans `build/alpha30-evidence/grand-user/`.
- Traversée réellement générée : 23 modules, 52 chunks FULL, 374 blocs entre
les accès ; parcours contrôlé sur 736 pas et 71 points, avec les collisions
natives, escaliers et raccords. Entrées : **192 238 -195** et **192 256 179**.
- Rivière : tracé entre des points distants denviron **865 blocs**, 6 924 cellules
de lit et de brèche, six ouvertures. Huit chunks FULL échantillonnés ; après
120 ticks, six échantillons contiennent de leau sous les deux sorties suivies.
Ce test ne prétend pas simuler toutes les chutes jusqu’à Y0.
- Sanctuaire minéral accepté dès la recherche intérieure ; emprise depuis
**-122 131 265**, 79 × 73 × 27. Le recours exposé nest pas exercé sur ce témoin.
- Quatre satellites, deux villageois, quatre coffres et une cascade d’îlot :
44 chunks FULL contrôlés. Les témoins de pillage, déplacement et démolition
sont préparés pour le contrôle de persistance.
- Le petit échantillon de structures vanilla ne contient pas de mine. Ce nest
pas un inventaire exhaustif de l’île et aucune présence de mine ny est annoncée.
## Petit 512, graine 2026
Processus **62463**, **24/24 GameTests passent**. Reçus :
`build/alpha30-evidence/small-2026-final2/`.
La traversée produit 15 modules et leurs départs natifs, avec 204 blocs entre
les entrées. Ce contrôle est limité aux plans et départs ; aucun parcours FULL
complet de cette traversée nest revendiqué sur Petit. La rivière couvre une
distance denviron 362 blocs, avec 2 551 cellules et quatre brèches. Sept chunks
FULL sont échantillonnés ; leau coule sous les sorties contrôlées.
Le sanctuaire possède son véritable départ de structure et son sol central
est contrôlé après génération au point **-54 162 69**, avec lespace libre
au-dessus. Les quatre satellites passent sur 41 chunks FULL.
Un premier essai a révélé que le témoin de vue de l’îlot à cascade se trouvait
sur une ancienne hauteur creusée en bassin. La correction choisit une berge
sèche proche, sans ajouter de support artificiel. Une première correction trop
large épuisait le budget de colonnes ; elle a été remplacée avant validation.
La commande finale dure 2 min 8 s.
## Moyen 724, graine 0
Processus **62604**, reçus `build/alpha30-evidence/medium-0/`.
La traversée réelle passe : **19 modules, 42 chunks FULL, 272 blocs entre les
accès, 456 pas et 62 points accessibles**. Entrées : **-136 248 -53** et
**136 260 -53**. La rivière s’étend sur environ 554 blocs : 3 942 cellules,
six brèches, neuf chunks FULL contrôlés et écoulement sous les sorties suivies.
Le sol central du sanctuaire est présent en **106 189 -63**, avec lespace
libre au-dessus ; les satellites passent également.
Le test exigeant une mine sur cette graine échoue : aucun départ de mine nest
retenu parmi ses 19 candidats, y compris via la recherche native. Les **23 autres
tests passent**. Le témoin de mine de la 29 nest donc pas réutilisé comme preuve
de présence en 30. Un témoin séparé est recherché, sans forcer une implantation
incompatible. Les refus conditionnels restent un comportement voulu du jeu.
## Grand 1 024, graine 42 — témoin de mine
Processus **62979**, **24/24 GameTests passent**, y compris lexigence de trouver
une mine réelle. Reçus dans `build/alpha30-evidence/grand-42/`.
Deux départs de mines vanilla sont observés dans les candidats examinés. La
mine visitée possède **83 pièces** ; l’échantillon de 16 chunks FULL du test
contient notamment **138 planches de chêne, 120 barrières, 43 rails et 23 toiles**.
Point de visite : **6 189 6**. Le test contrôle les pièces sauvegardées et leur
aller-retour NBT ; il ne visite pas lintégralité de la mine et ne garantit pas
un spawner dans chaque corridor. Une lecture séparée de la sauvegarde confirme
un véritable spawner `cave_spider` en **46 188 25** dans un corridor :
`build/alpha30-evidence/grand-42/saved-spawners.json`. Son activation par un joueur
na pas été testée.
Deux huttes de sorcière sont également observées. La hutte visitée est en
**83 148 -396**, avec ses pièces, appuis et son mobilier vanilla.
La traversée possède **25 modules répartis sur 24 départs natifs**, avec 374 blocs entre
les entrées ; cette graine utilise le contrôle des plans, sans nouveau parcours
FULL de toute la traversée. Le sanctuaire possède son vrai départ et son sol
central. La rivière s’étend sur environ **882 blocs**, avec 5 822 cellules et
six ouvertures ; les écoulements suivis passent sous ticks réels.
`./gradlew check build` passe sur ce témoin : **88 tâches**, dont les tests purs,
les contrôles de sérialisation et les **24 GameTests natifs**, en **6 min 28 s**.
Le coût inclut toute la suite et nest pas un temps de chargement de partie.
Commande :
```sh
./gradlew check build -PsanctuaryTestDiameter=1024 -PsanctuaryTestSeed=42 \
-PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true \
-PsanctuaryTransit29Tests=true -PsanctuaryTransit29PlanOnly=true \
-PsanctuaryRequireMineshaft29=true
```
## Relief
Sur ce Grand, 8 112 densités sous Y180 sont identiques bit à bit à la 28.
Dans les coupes hautes, 4 544 / 94 640 échantillons changent de signe, soit
**4,80 %**. Deux coupes au pas dun bloc, Z0 et Z96, ont été rendues et examinées :
les plateaux redeviennent larges entre les zones où subsistent les surplombs.
Aperçu local `build/alpha30-preview/relief-1024--7228211907433324401.png`.
Les mêmes coupes ont aussi été examinées sur Petit2026 : 602 / 40 460 signes
modifiés (1,49 %), avec des sommets plus plats entre les zones déformées.
Ce contrôle de densité ne remplace pas une exploration visuelle dans le client
et ne démontre pas labsence de toute petite pointe ou masse déconnectée.
## Tests ciblés et limites
Le test pur `river30Smoke` couvre trois rayons sur une masse rocheuse synthétique :
longueur du tracé, lit dans la roche, arrivée protégée, brèches de cinq blocs
maximum, déterminisme et espaces non déformés entre les trois secteurs.
Ce nest pas une matrice de neuf mondes Minecraft natifs.
La recherche reste bornée par la taille de l’île et le nombre de candidats.
Sur le Grand, la commande native complète dure **4 min 44 s**, incluant le
lancement, la planification, les structures et les nombreux chunks demandés
par les tests. Ce temps nest ni un temps de rendu client, ni un benchmark
comparatif avec la 28. Aucun objectif chiffré de performance nest revendiqué.
Les tests vérifient des occurrences et parcours précis, pas une garantie
universelle sur toutes les graines. Les mines restent conditionnelles, et le
sanctuaire peut encore être refusé si aucune implantation na dattache valable.
Les réglages, le Blocodex et la suite générale restent soumis à `check build`.
## Redémarrage et artefacts
Grand1 024, graine42, processus **63276** : **24/24 tests passent après un
redémarrage réel** (2 min 14 s). Les 47 chunks FULL du contrôle aérien conservent
coffres pillés, villageois déplacés et destructions préparées à la création,
sans duplication ni reconstruction des témoins. Les départs de mine et leurs
pièces, le sol du sanctuaire et les écoulements échantillonnés passent à nouveau.
Le journal dexpansion reste inchangé. Preuves :
`build/alpha30-evidence/grand-42-reload/` et `build/alpha30-reload.log`.
`./gradlew assemblePack` passe après `check build` (le second lancement exclut
la tâche `:sanctuary:check` déjà réussie). Le JAR, le MRpack autonome et le ZIP
damorçage Prism passent la vérification locale : versions, dépendances exactes,
index et empreintes concordent. Reçus : `build/alpha30-artifacts.json`,
`build/alpha30-delivery-local.json`. Les fichiers du client Windows nont pas
été modifiés. La publication et la synchronisation de linstance Mac sont
suivies dans [la distribution](packwiz.md).
+2 -2
View File
@@ -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.27
pack_version=0.1.0-alpha.27
mod_version=0.1.0-alpha.30
pack_version=0.1.0-alpha.30
maven_group=fr.koka.sanctuary
+42
View File
@@ -56,6 +56,10 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.generateStructures', providers.gradleProperty('sanctuaryGenerateStructures').getOrElse('true'))
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
systemProperty('sanctuary.test.transit29', providers.gradleProperty('sanctuaryTransit29Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit29.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').getOrElse('false'))
systemProperty('sanctuary.test.requireMineshaft29', providers.gradleProperty('sanctuaryRequireMineshaft29').getOrElse('false'))
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
@@ -623,3 +627,41 @@ tasks.register('swamp27VolumesSmoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
}
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
tasks.register('cavesRelief28Smoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavesRelief28Smoke'
}
tasks.named('check') { dependsOn('cavesRelief28Smoke') }
// Fine ledges retain the alpha28 field below the upper relief.
tasks.register('terraces29Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.Terraces29Smoke'
}
tasks.named('check') { dependsOn('terraces29Smoke') }
[
transit29Smoke: 'fr.koka.sanctuary.worldgen.transit.Transit29Smoke',
mineshaftAdmission29Smoke: 'fr.koka.sanctuary.expansion.MineshaftAdmission29Smoke'
].each { taskName, harness ->
tasks.register(taskName, JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = harness
}
tasks.named('check') { dependsOn(taskName) }
}
tasks.register('river30Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
}
tasks.named('check') { dependsOn('river30Smoke') }
@@ -44,7 +44,9 @@ public final class Aerial24FlowAdmissionGameTests {
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));
if (!requests(level.getSeed(), session.islands(), plan, true).isEmpty())
admitted.add(admit(helper, level, generator, session, plan, true, report));
else report.put("collision_admission", "No raw expansion candidate reaches the close sites; initial island ownership already excludes them");
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; }
@@ -88,15 +90,17 @@ public final class Aerial24FlowAdmissionGameTests {
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");
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks beneath the suspended island");
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);
// Alpha28 islets stand above real terrain: a cascade may land on Sanctuary.
// Require native flow through the free air gap, not an impossible route through rock.
var below = waterAt(level, waterfall, waterfall.minY() - 1);
var low = waterAt(level, waterfall, waterfall.minY() - AerialPlanner24.MIN_ISLET_GAP);
report.put("water_below_islet", below == null ? List.of() : coords(below));
report.put("water_near_y0", low == null ? List.of() : coords(low));
report.put("water_across_air_gap", 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.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall through the sixteen-block air gap");
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");
@@ -128,7 +132,7 @@ public final class Aerial24FlowAdmissionGameTests {
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));
@@ -149,7 +153,7 @@ public final class Aerial24FlowAdmissionGameTests {
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;
if (filtered == null) continue;
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
}
return result;
@@ -112,6 +112,12 @@ public final class Aerial24WorldGameTests {
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
if (!report.containsKey("native_departures_ready")) {
// Fail on a missing native start immediately instead of waiting for entities
// which could never have been placed (for example, an obsolete void-only gate).
report.put("stage", "verify saved aerial departures before entity loading");
report.put("native_departures_ready", starts(helper, level, plan).size());
}
if (!tickets[0]) {
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
}
@@ -170,14 +176,25 @@ public final class Aerial24WorldGameTests {
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");
if (site.merchant()) {
double gap = AerialPlanner24.shoreDistance(site);
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
} else {
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
int highest = 0;
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) { highest = y; break; }
helper.assertTrue(site.minY() - highest - 1 >= AerialPlanner24.MIN_ISLET_GAP, "At least sixteen air blocks separate ground and suspended rock");
}
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,
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance, "hull_to_shore", AerialPlanner24.shoreDistance(site),
"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)),
@@ -223,8 +240,17 @@ public final class Aerial24WorldGameTests {
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()));
if (site.merchant()) {
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()));
} else {
// Natural trees may occupy the mathematical ground reference below an islet.
// Observe a real nearby walking position without removing that vegetation.
var viewpoint = nearbyGroundView(level, shore);
helper.assertTrue(viewpoint != null, "A walkable ground viewpoint exists within twelve horizontal blocks below the suspended islet: " + site.id());
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column,
"walkable_viewpoint", coordinates(viewpoint), "reference_obstruction", level.getBlockState(shore.above()).toString()));
}
var chestAt = AerialGeometry24.chest(site);
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
@@ -246,12 +272,15 @@ public final class Aerial24WorldGameTests {
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");
}
// Root terrain and its own trees may now exist outside these aerial envelopes.
// Chunk clipping is checked against every authored position; do not confuse legal
// surrounding terrain with a structure leak by requiring a whole exterior air column.
helper.assertTrue(model.keySet().stream().allMatch(site.bounds()::isInside), "Every proposed structure block is inside its finite envelope");
var exterior = new ArrayList<Map<String, Object>>();
for (var at : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2),
new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
report.put(site.id() + "_exterior_observations", exterior);
}
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
@@ -259,6 +288,21 @@ public final class Aerial24WorldGameTests {
return expected;
}
private static BlockPos nearbyGroundView(ServerLevel level, BlockPos reference) {
for (int radius = 0; radius <= 12; radius++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != radius || Math.hypot(dx, dz) > 12) continue;
for (int dy = 8; dy >= -8; dy--) {
var at = reference.offset(dx, dy, dz);
var floor = level.getBlockState(at.below());
if (!floor.isSolid() || floor.is(net.minecraft.tags.BlockTags.LEAVES) || floor.is(net.minecraft.tags.BlockTags.LOGS)) continue;
if (level.getBlockState(at).getCollisionShape(level, at).isEmpty()
&& level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()
&& level.getFluidState(at).isEmpty() && level.getFluidState(at.above()).isEmpty()) return at;
}
}
return null;
}
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,
@@ -0,0 +1,239 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionStructures23;
import fr.koka.sanctuary.expansion.SwampHutPlacement28;
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** Observes normal root generation only. No start injection, fake room, expansion or seed search. */
public final class Alpha28NativeStructuresGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path OUT = Path.of("diagnostics/native28");
@GameTest(maxTicks = 3600)
public void actualNativeStructures(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try {
var level = helper.getLevel(); var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
report.put("seed", level.getSeed()); report.put("diameter", host.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures()) {
report.put("scope", "Native structures globally disabled"); report.put("passed", true); write(report); helper.succeed(); return;
}
var root = host.source().islands().stream().filter(island -> island.origin()).findFirst().orElseThrow();
var candidates = ExpansionStructures23.candidates(root).stream().limit(24).toList();
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
var firstTwelve = new TreeMap<String, Integer>(); int candidateIndex = 0;
for (var candidate : candidates) {
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
for (var start : chunk.getAllStarts().values()) {
if (!start.isValid()) continue;
String id = registry.getKey(start.getStructure()).toString(); observed.merge(id, 1, Integer::sum);
if (candidateIndex < 12) firstTwelve.merge(id, 1, Integer::sum);
if (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
}
candidateIndex++;
}
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
report.put("first_twelve_starts", firstTwelve);
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29") && found.keySet().stream().noneMatch(id -> id.contains("mineshaft"))) {
var islandsBefore = List.copyOf(host.source().islands());
var wanted = net.minecraft.core.HolderSet.direct(registry.get(Identifier.parse("minecraft:mineshaft")).orElseThrow(),
registry.get(Identifier.parse("minecraft:mineshaft_mesa")).orElseThrow());
// Same finite real-start path as /locate; no injected plan, reference increment or expansion.
var located = ExpansionStructures23.findNearest(level, wanted, BlockPos.ZERO, 8, false);
helper.assertTrue(islandsBefore.equals(host.source().islands()), "Native /locate never adds an expansion or changes the island descriptors");
var locateProof = new LinkedHashMap<String, Object>();
locateProof.put("invoked", true); locateProof.put("found", located != null);
locateProof.put("islands_unchanged", true); locateProof.put("skip_referenced", false);
locateProof.put("root_candidates_available", ExpansionStructures23.candidates(root).size());
report.put("locate_mineshaft", locateProof);
if (located != null) {
String id = located.getSecond().unwrapKey().orElseThrow().identifier().toString();
for (var candidate : ExpansionStructures23.candidates(root)) {
var start = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true)
.getStartForStructure(located.getSecond().value());
if (start != null && start.isValid() && start.getPieces().getFirst().getLocatorPosition().equals(located.getFirst())) {
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.chunk().x(), candidate.chunk().z())); break;
}
}
helper.assertTrue(found.containsKey(id), "The native /locate result belongs to a real saved start");
locateProof.put("id", id); locateProof.put("position", located.getFirst().toShortString());
}
} else report.put("locate_mineshaft", Map.of("invoked", false));
var full = new LinkedHashSet<ChunkPos>();
// Visit actual corridor bodies (including both endpoint chunks), not just four
// piece centres that could all belong to unadorned rooms. No graph is truncated.
var pieceContext = StructurePieceSerializationContext.fromLevel(level);
int mineFamilies = (int) found.keySet().stream().filter(id -> id.contains("mineshaft")).count();
for (var entry : found.entrySet()) {
full.add(entry.getValue().getChunkPos());
var pieces = entry.getValue().getPieces();
if (entry.getKey().contains("mineshaft")) pieces = pieces.stream()
.filter(piece -> piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor)
.sorted(Comparator.comparingInt(piece -> {
var tag = piece.createTag(pieceContext);
return tag.getBoolean("hr").orElse(false) ? 0 : tag.getBoolean("sc").orElse(false) ? 1 : 2;
})).limit(mineFamilies > 1 ? 2 : 4).toList();
for (var piece : pieces) {
var proposed = new LinkedHashSet<>(full); piece.getBoundingBox().intersectingChunks().forEach(proposed::add);
if (proposed.size() <= 24) full.addAll(proposed);
}
}
// A few additional real generation chunks can contain native monster_room attempts.
for (var candidate : candidates) {
if (full.size() >= 16) break;
full.add(candidate.chunk());
}
helper.assertTrue(full.size() <= 24, "Bounded native structure witness requests at most24 FULL chunks");
var iterator = full.iterator(); Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var at = iterator.next(); report.put("stage", "FULL " + at);
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
inspect(helper, level, found, full, report);
report.put("passed", true); write(report); helper.succeed();
} catch (Exception error) { failure(report, error); throw new IllegalStateException("Native28 witness failed", error); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
} catch (Exception error) { failure(report, error); throw error; }
}
private static void inspect(GameTestHelper helper, ServerLevel level, Map<String, StructureStart> found,
Set<ChunkPos> full, Map<String, Object> report) {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var context = StructurePieceSerializationContext.fromLevel(level);
var proofs = new LinkedHashMap<String, Object>();
var census = StructureCensusService.snapshot(level.getServer()).evidence().sites().values();
var monsterPositions = new HashSet<BlockPos>();
for (var site : census) {
var key = site.key();
if (key.id().equals(StructureCensusMemory.MONSTER_ROOM) && key.dimension().equals(level.dimension().identifier().toString()))
monsterPositions.add(new BlockPos(key.x(), key.y(), key.z()));
}
for (var entry : found.entrySet()) {
var expected = entry.getValue(); var cp = expected.getChunkPos();
var actual = level.getChunk(cp.x(), cp.z(), ChunkStatus.FULL, true)
.getStartForStructure(registry.getValue(Identifier.parse(entry.getKey())));
helper.assertTrue(actual != null && actual.isValid(), "The real selected native start survives FULL: " + entry.getKey());
var tag = actual.createTag(context, cp);
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Native piece NBT roundtrip: " + entry.getKey());
var counts = new TreeMap<String, Integer>(); var positions = new HashSet<BlockPos>();
var spawners = new ArrayList<Object>(); var corridorFlags = new ArrayList<Object>();
var spawnerTypes = new HashMap<BlockPos, String>();
for (var piece : actual.getPieces()) {
var b = piece.getBoundingBox();
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
var flags = piece.createTag(context);
corridorFlags.add(Map.of("box", b.toString(), "fully_visited", b.intersectingChunks().allMatch(full::contains),
"rails", flags.getBoolean("hr").orElse(false), "spider_corridor", flags.getBoolean("sc").orElse(false),
"spawner_placed", flags.getBoolean("hps").orElse(false)));
}
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
for (int y = b.minY() - 1; y <= b.maxY() + 1; y++) {
BlockPos pos = new BlockPos(x, y, z); if (!positions.add(pos)) continue;
var state = level.getBlockState(pos);
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
if (state.is(Blocks.SPAWNER)) {
helper.assertTrue(level.getBlockEntity(pos) instanceof SpawnerBlockEntity, "A mine spawner has its actual native block entity");
var saved = ((SpawnerBlockEntity) level.getBlockEntity(pos)).saveWithFullMetadata(level.registryAccess());
String mob = saved.getCompoundOrEmpty("SpawnData").getCompoundOrEmpty("entity").getStringOr("id", "");
spawnerTypes.put(pos, mob);
String origin = monsterPositions.contains(pos) ? "monster_room" : "mineshaft";
if (entry.getKey().contains("mineshaft")) {
if (origin.equals("monster_room")) helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").contains(mob),
"A recorded vanilla dungeon inside the mine retains its own monster type");
else {
var flags = piece.createTag(context);
helper.assertTrue(mob.equals("minecraft:cave_spider") && flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false),
"A native mine spawner agrees with its cave-spider piece flags");
}
}
spawners.add(Map.of("position", pos.toShortString(), "origin", origin, "entity", mob, "nbt", saved.toString()));
}
}
}
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
var flags = piece.createTag(context);
if (flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false) && b.intersectingChunks().allMatch(full::contains)) {
boolean actualSpider = spawnerTypes.entrySet().stream().anyMatch(e -> b.isInside(e.getKey()) && e.getValue().equals("minecraft:cave_spider"));
// A later successful native monster_room may replace a mine's spawner. Its real
// census coordinate must then lie on this corridor's native spawner centreline.
boolean recordedReplacement = false;
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? b.getZSpan() : b.getXSpan();
for (int z = 1; z < length; z++) {
var at = SwampHutPlacement28.worldPosition(piece,1,0,z);
if (monsterPositions.contains(at) && spawnerTypes.containsKey(at)) recordedReplacement = true;
}
helper.assertTrue(actualSpider || recordedReplacement, "A fully visited sc/hps corridor has its actual cave-spider spawner or an evidenced later vanilla dungeon");
}
}
if (entry.getKey().equals("minecraft:swamp_hut")) for (var pier : SwampHutPlacement28.piers(piece)) {
var state = level.getBlockState(pier);
helper.assertTrue(state.is(Blocks.OAK_LOG) || state.isSolid(), "A native hut pier has wood or a real solid foot: " + pier);
}
}
if (entry.getKey().contains("mineshaft")) helper.assertTrue(counts.entrySet().stream()
.filter(e -> e.getKey().endsWith("_planks") || e.getKey().endsWith("_fence") || e.getKey().equals("minecraft:rail"))
.mapToInt(Map.Entry::getValue).sum() > 0, "A selected mine creates actual native timber/rail geometry");
proofs.put(entry.getKey(), Map.of("start_chunk", List.of(cp.x(), cp.z()), "pieces", actual.getPieces().size(),
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString(),
"corridor_flags", corridorFlags, "spawners", spawners));
}
var rooms = new ArrayList<Object>();
for (var site : census) {
var key = site.key();
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
if (!full.contains(cp)) continue;
var at = new BlockPos(key.x(), key.y(), key.z());
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
var spawnerNbt = ((SpawnerBlockEntity) level.getBlockEntity(at)).saveWithFullMetadata(level.registryAccess());
helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").stream().anyMatch(spawnerNbt.toString()::contains), "Vanilla dungeon retains a real vanilla monster type");
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true, "spawner_nbt", spawnerNbt.toString()));
}
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
boolean mineFound = found.keySet().stream().anyMatch(id -> id.contains("mineshaft"));
report.put("mineshaft_found", mineFound);
report.put("mineshaft_required", Boolean.getBoolean("sanctuary.test.requireMineshaft29"));
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
report.put("monster_room_found", !rooms.isEmpty());
report.put("scope", "Sample of24 normal start candidates, optional real /locate when a mine is required, and at most24 requested FULL chunks; the first12 remain separately reported. Corridor flags and optional spawners do not impose random-content quotas");
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29"))
helper.assertTrue(mineFound, "The required mine witness has a real native start and inspected FULL timber/rail geometry");
}
private static void write(Map<String, Object> report) throws Exception {
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
}
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("error", error.toString());
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
catch (Exception io) { error.addSuppressed(io); }
SanctuaryMod.LOGGER.error("Native28 witness failed", error);
}
}
@@ -0,0 +1,117 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Bounded comparison of the real registered density, followed by inspection of ordinary FULL chunks. */
public final class Caves28WorldGameTests {
@GameTest(maxTicks = 3600)
public void geologyAndNativeOres(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var binding = generator.starterContext();
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.geology28(), "Current world uses the native-cave adaptation");
var current = binding.random().getSampler(field);
var previous = binding.random().getSampler(field.legacy27());
var context = SamplerContext.builder().enableCaches().build();
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("scope", "Registered density comparison; no synthetic terrain, water, ore or room is placed by the test");
var heights = new ArrayList<List<Integer>>();
long oldSolid = 0, remainingSolid = 0, excavated = 0, restored = 0;
int min = -Math.min(320, generator.initialDiameter() / 2), max = -min, raised = 0, lowered = 0, largestUplift = 0, largestSubsidence = 0;
for (int z = min; z <= max; z += 32) for (int x = min; x <= max; x += 32) {
int oldTop = 0, newTop = 0;
for (int y = 2; y <= 382; y += 2) {
boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0;
if (old) oldTop = y; if (now) newTop = y;
if (y >= 48 && y <= 202) {
if (old) { oldSolid++; if (now) remainingSolid++; else excavated++; }
else if (now) restored++;
}
if (Math.hypot(x, z) < 40 && y >= 240)
helper.assertTrue(old == now, "High arrival neighbourhood remains unchanged");
}
if (newTop > oldTop && oldTop > 208) { raised++; largestUplift = Math.max(largestUplift, newTop - oldTop); }
if (newTop < oldTop && oldTop > 240) { lowered++; largestSubsidence = Math.max(largestSubsidence, oldTop - newTop); }
heights.add(List.of(x, z, oldTop, newTop));
}
report.put("heights", heights); report.put("old_solid_samples", oldSolid);
report.put("remaining_solid_samples", remainingSolid); report.put("excavated_samples", excavated);
report.put("restored_rift_samples", restored); report.put("raised_columns", raised);
report.put("largest_uplift", largestUplift);
report.put("lowered_columns", lowered); report.put("largest_subsidence", largestSubsidence);
helper.assertTrue(excavated > 0 && remainingSolid > excavated, "New passages exist while most sampled former rock remains");
helper.assertTrue(raised > 0, "Native volumetric deformation raises some uplands");
var childField = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
var child = new fr.koka.sanctuary.expansion.ExpansionIsland("geology_witness", "sanctuary", "east", "temperate",
512, 2048, 0, level.getSeed(), "natural", "ready", 24);
var childSettings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"))).value();
var childState = net.minecraft.world.level.levelgen.RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), child.seed(), childSettings);
var childNew = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField, child));
var childOld = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField.legacy27(), child));
long childExcavated = 0, childRemaining = 0;
for (int z = -160; z <= 160; z += 32) for (int dx = -160; dx <= 160; dx += 32)
for (int y = 48; y <= 202; y += 4) {
if (childOld.sampleValue(context, 2048 + dx, y, z) > 0) {
if (childNew.sampleValue(context, 2048 + dx, y, z) <= 0) childExcavated++;
else childRemaining++;
}
}
report.put("expansion_density", Map.of("diameter", 512, "excavated", childExcavated,
"remaining", childRemaining, "reserved_or_opened", false));
helper.assertTrue(childExcavated > 0 && childRemaining > childExcavated, "The actual expansion wrapper uses the same finite cave field");
var sections = new LinkedHashMap<String, Object>();
for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 382; y >= 0; y -= 2) {
var a = new StringBuilder(); var b = new StringBuilder();
for (int x = min; x <= max; x += 2) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
}
before.add(a.toString()); after.add(b.toString());
}
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", min, "step", 2));
}
report.put("sections", sections);
var ores = new TreeMap<String, Integer>(); var oreDepths = new TreeMap<String, List<Integer>>();
for (int cx : new int[]{-6, 0, 6}) for (int cz : new int[]{-6, 0, 6}) {
var chunk = level.getChunk(cx, cz, ChunkStatus.FULL, true);
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 1; y < 383; y++) {
var state = chunk.getBlockState(new BlockPos(cx * 16 + x, y, cz * 16 + z));
String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if (id.endsWith("_ore")) {
ores.merge(id, 1, Integer::sum);
var band = oreDepths.computeIfAbsent(id, ignored -> new ArrayList<>(List.of(384, 0)));
band.set(0, Math.min(band.get(0), y)); band.set(1, Math.max(band.get(1), y));
}
}
}
report.put("full_chunks_inspected", 9); report.put("ores", ores); report.put("ore_y_ranges", oreDepths);
helper.assertTrue(!ores.isEmpty(), "Actual generated rock contains native ore deposits");
report.put("passed", true);
Path path = Path.of("diagnostics/alpha28/geology.json"); Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
}
}
@@ -23,7 +23,7 @@ import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
/** Compares the explicit legacy27 mode against frozen alpha10, not the intentional alpha28 terrain changes. */
final class PopulationDensityEquivalenceDiagnostics {
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
@@ -39,7 +39,7 @@ final class PopulationDensityEquivalenceDiagnostics {
var key = SanctuaryMod.id("population_" + players);
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
var density = (PopulationIslandDensity) registered;
var density = ((PopulationIslandDensity) registered).legacy27();
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
@@ -113,7 +113,7 @@ final class PopulationDensityEquivalenceDiagnostics {
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
"scope", "Explicit legacy27 mode versus frozen alpha10: real compiled scalar/bulk densities, 4 capacities x 3 seeds, signed coordinates, region/coast/height boundaries, context switches and concurrency. Geology28 is intentionally disabled only for this historical equivalence proof; no alpha28 generation claim.")) + "\n");
}
private static List<DensityVolume> volumes(IslandCapacity capacity) {
@@ -87,6 +87,7 @@ public final class PopulationDiagnostics {
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
var island = (PopulationIslandDensity) density;
var legacyEnvelope = island.legacy27();
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
var random = binding.random();
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
@@ -102,7 +103,10 @@ public final class PopulationDiagnostics {
float value = random.sampleBlockValueUncached(density, x, y, z);
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
float historical = island.geology28()
? random.sampleBlockValueUncached(legacyEnvelope, x, y, z) : value;
helper.assertTrue(historical <= 0 || raw > 0,
"The explicit legacy27 envelope never fills its raw natural holes; alpha28 uplands change sample Y");
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
if (x % (step * 8) == 0 && z % (step * 8) == 0)
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
@@ -0,0 +1,62 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.worldgen.*;
import java.nio.file.*;
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.TicketType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
public final class River30WorldGameTests {
@GameTest(maxTicks=12000)
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.river30(binding.generator(),binding.random());
var report=new LinkedHashMap<String,Object>();report.put("seed",level.getSeed());report.put("diameter",generator.initialDiameter());
report.put("process_id",ProcessHandle.current().pid());report.put("river_cells",plan.cells().size());report.put("features",plan.features());report.put("spills",plan.spills());
report.put("mineral",generator.mineralSanctuary23());
helper.assertTrue(plan.features().stream().anyMatch(f->f.kind()==PopulationHydrology.Kind.RIVER),"An island-wide river must be present in the release witness");
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
helper.assertFalse(generator.mineralSanctuary23().isEmpty(),"The mineral sanctuary must be present in the release witness");
var selected=plan.cells().stream().filter(PopulationHydrology.Cell::hasWater).toList();
var witnesses=new ArrayList<PopulationHydrology.Cell>();
for(int i=0;i<6;i++)witnesses.add(selected.get(i*(selected.size()-1)/5));
var falls=plan.spills().stream().limit(2).toList();var chunks=new LinkedHashSet<ChunkPos>();
witnesses.forEach(c->chunks.add(new ChunkPos(c.x()>>4,c.z()>>4)));
for(var fall:falls){chunks.add(new ChunkPos(fall.source().x()>>4,fall.source().z()>>4));chunks.add(new ChunkPos(fall.outlet().x()>>4,fall.outlet().z()>>4));}
var mineral=generator.mineralSanctuary23().chamber();
if(mineral!=null){
var key=fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.KEY;
var structure=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.STRUCTURE).getOrThrow(key).value();
var cp=new ChunkPos(mineral.startChunkX(),mineral.startChunkZ());
var start=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(structure);
helper.assertTrue(start!=null&&start.isValid()&&start.getPieces().size()==1,"The mineral plan produces its actual native structure start");
chunks.add(cp);
}
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];
step[0]=()->{
if(iterator.hasNext()) {var c=iterator.next();level.getChunk(c.x(),c.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
chunks.forEach(c->level.getChunkSource().addTicketWithRadius(TicketType.DRAGON,c,2));
helper.runAtTickTime(helper.getTick()+120,()->{
try {
if(mineral!=null){
var floor=new BlockPos(mineral.centerX(),mineral.minY()+2,mineral.centerZ());
helper.assertTrue(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir(),"The central mineral floor and guardian space are really generated");
report.put("mineral_native_floor",List.of(floor.getX(),floor.getY(),floor.getZ()));
}
for(var c:witnesses)helper.assertTrue(!level.getFluidState(new BlockPos(c.x(),c.waterY(),c.z())).isEmpty(),"Generated river water remains at its supported local bed");
int flowing=0;
for(var fall:falls)for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
if(!level.getFluidState(new BlockPos(fall.outlet().x()+dx,fall.outlet().y()-3,fall.outlet().z()+dz)).isEmpty()){flowing++;}
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
report.put("full_chunks",chunks.size());report.put("flowing_below_openings",flowing);report.put("passed",true);
Path out=Path.of("diagnostics/alpha30/river.json");Files.createDirectories(out.getParent());Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();
}catch(Exception error){throw new RuntimeException(error);}finally{chunks.forEach(c->level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON,c,2));}
});
};step[0].run();
}
}
@@ -0,0 +1,68 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares registered native fields; does not place blocks or load additional chunks. */
public final class Terraces29WorldGameTests {
@GameTest(maxTicks = 2400)
public void fineLedgesPreserveTheAcceptedMass(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.relief30() && !field.terraces29(), "Alpha30 localizes deformation and disables periodic micro-ledges");
var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
var context = SamplerContext.builder().enableCaches().build();
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid());
int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0;
for (int z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) {
for (int y = 32; y <= 180; y += 13) {
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Lower density is exactly alpha28");
lowerSamples++;
}
for (int y = 181; y <= 320; y++) {
boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0;
if (old == now) unchanged++; else changed++;
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
}
}
helper.assertTrue(changed > 0 && unchanged > changed * 3L, "Localized relief retains most of the accepted upper mass");
report.put("unchanged_lower_density_samples", lowerSamples);
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
var sections = new LinkedHashMap<String, Object>();
for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 305; y >= 205; y--) {
var a = new StringBuilder(); var b = new StringBuilder();
for (int x = -radius; x <= radius; x++) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
}
before.add(a.toString()); after.add(b.toString());
}
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 305, "step", 1));
}
report.put("sections", sections); report.put("passed", true);
Path path = Path.of("diagnostics/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
}
}
@@ -342,15 +342,18 @@ public final class Transit21WorldGameTests {
static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
var level = helper.getLevel();
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
boolean fitted29 = plan.diagnostic().startsWith("accepted29");
int approachHalfWidth = fitted29 ? 1 : 2;
int approachDepth = fitted29 ? 1 : 3;
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - approachHalfWidth && pos.getX() <= m.centerX() + approachHalfWidth
&& pos.getZ() >= m.minZ() - approachDepth && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var start = TransitPiece21.locator(accesses.getFirst());
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
pos.getX() >= m.centerX() - approachHalfWidth && pos.getX() <= m.centerX() + approachHalfWidth
&& pos.getZ() >= m.minZ() - approachDepth && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
var arrivals = new ArrayList<Map<String, Object>>();
@@ -364,7 +367,8 @@ public final class Transit21WorldGameTests {
var gate = TransitPiece21.locator(access);
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
int approaches = 0;
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
for (int x = access.centerX() - approachHalfWidth; x <= access.centerX() + approachHalfWidth; x++)
for (int z = access.minZ() - approachDepth; z < access.minZ(); z++) {
var high = new BlockPos(x, access.surfaceY() + 1, z);
var low = high.below(); var position = standable(level, high) ? high : low;
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
@@ -0,0 +1,130 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.transit.*;
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.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** One actual island and its published route. No injected starts, synthetic terrain or expansion. */
public final class Transit29WorldGameTests {
private static final Path OUT = Path.of("diagnostics/transit29");
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 12000)
public void throughRouteHasTwoNativeAccesses(GameTestHelper helper) throws Exception {
if (!Boolean.getBoolean("sanctuary.test.transit29")) { helper.succeed(); return; }
var report = new LinkedHashMap<String, Object>();
try {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var plan = generator.transitPlan21();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("planner", plan.diagnostic());
report.put("modules", plan.modules().size()); report.put("probes", plan.samples());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty(), "Structures OFF leaves no transit reservation");
report.put("passed", true); write(report); helper.succeed(); return;
}
helper.assertFalse(plan.isEmpty(), "A structures-enabled Sanctuary world must retain its main traversal: " + plan.diagnostic());
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "The complete traversal has two independent surface accesses");
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
helper.assertTrue(span >= generator.initialDiameter() / 4, "The main route spans at least a quarter of the island diameter");
report.put("span", span);
var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var starts = new LinkedHashMap<ChunkPos, Integer>();
var context = StructurePieceSerializationContext.fromLevel(level);
var biomes = new TreeMap<String, Integer>();
var chunks = new LinkedHashSet<ChunkPos>();
for (var module : plan.modules()) {
var cp = new ChunkPos(module.startChunkX(), module.startChunkZ());
if (!starts.containsKey(cp)) {
var chunk = level.getChunk(cp.x(), cp.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(holder.value());
helper.assertTrue(start != null && start.isValid(), "Every planned local transit start exists: " + cp);
int expected = plan.modulesForChunk(cp.x(), cp.z()).size();
helper.assertTrue(start.getPieces().size() == expected, "No planned module was lost during native structure generation");
var tag = start.createTag(context, cp);
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Every native transit start roundtrips its full saved geometry");
for (var piece : start.getPieces())
helper.assertFalse(piece instanceof TransitPiece21 transit
&& transit.module().kind() == TransitPlan21.Kind.JUNCTION,
"Junctions close their unopened walls with the corrected renderer");
starts.put(cp, expected);
}
TransitPiece21.bounds(module).intersectingChunks().forEach(chunks::add);
if (module.kind() == TransitPlan21.Kind.ACCESS) {
for (int x = module.centerX() - 2; x <= module.centerX() + 2; x++)
for (int z = module.minZ() - 3; z < module.minZ(); z++) chunks.add(new ChunkPos(x >> 4, z >> 4));
}
}
helper.assertTrue(chunks.size() <= 160, "The native traversal witness stays inside its bounded footprint");
report.put("local_starts", starts.size()); report.put("planned_footprint_chunks", chunks.size());
boolean planOnly = Boolean.getBoolean("sanctuary.test.transit29.planOnly");
report.put("plan_only", planOnly);
if (planOnly) {
report.put("full_chunks", 0);
report.put("surface_accesses", accesses.stream().map(m -> TransitPiece21.locator(m).toShortString()).toList());
report.put("scope", "Published native plan and all STRUCTURE_STARTS with saved-piece roundtrip; no FULL blocks or physical traversal checked in this case");
report.put("passed", true); write(report); helper.succeed(); return;
}
report.put("full_chunks", chunks.size());
var iterator = chunks.iterator(); Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var at = iterator.next(); report.put("stage", "FULL " + at);
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
for (var module : plan.modules()) {
var at = TransitPiece21.locator(module);
biomes.merge(level.getBiome(at).getRegisteredName(), 1, Integer::sum);
}
int closedWallChecks = 0;
for (var module : plan.modules()) if (module.kind() == TransitPlan21.Kind.JUNCTION)
for (int direction = 0; direction < 4; direction++) if ((module.openings() & 1 << direction) == 0)
for (int across = -2; across <= 2; across++) {
int x = direction == 1 ? module.maxX() : direction == 3 ? module.minX() : module.centerX() + across;
int z = direction == 0 ? module.minZ() : direction == 2 ? module.maxZ() : module.centerZ() + across;
var at = new net.minecraft.core.BlockPos(x, module.minY() + 2, z);
helper.assertTrue(level.getBlockState(at).isSolid(), "An unopened junction wall remains solid at " + at);
closedWallChecks++;
}
report.put("closed_wall_checks", closedWallChecks);
report.put("module_biomes", biomes);
report.put("walk", Transit21WorldGameTests.verifyAccess(helper, plan));
var nearest = TransitStructures21.findNearest(level, HolderSet.direct(holder), net.minecraft.core.BlockPos.ZERO, 128, false);
helper.assertTrue(nearest != null && accesses.stream().anyMatch(m -> TransitPiece21.locator(m).equals(nearest.getFirst())),
"The real structure locator returns one of the walkable surface gates");
report.put("locate", nearest.getFirst().toShortString());
report.put("passed", true); write(report); helper.succeed();
} catch (Exception failure) { fail(report, failure); throw new IllegalStateException("Transit29 native traversal failed", failure); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
} catch (Exception failure) { fail(report, failure); throw failure; }
}
private static void write(Map<String, Object> report) throws Exception {
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
}
private static void fail(Map<String, Object> report, Exception failure) {
report.put("passed", false); report.put("error", failure.toString());
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
catch (Exception io) { failure.addSuppressed(io); }
SanctuaryMod.LOGGER.error("Transit29 native traversal failed", failure);
}
}
@@ -6,7 +6,7 @@
"environment": "*",
"license": "GPL-3.0-or-later",
"entrypoints": {
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -146,9 +146,17 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
}
@Override public void applyBiomeDecoration(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
var island = islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (!ExpansionOcean23.active(island)) { super.applyBiomeDecoration(level, chunk, structures); return; }
fr.koka.sanctuary.worldgen.IslandOres28.place(level, this, chunk, island);
if (!ExpansionOcean23.active(island)) {
super.applyBiomeDecoration(level, chunk, structures);
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
return;
}
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
try { super.applyBiomeDecoration(level, chunk, structures); }
try {
super.applyBiomeDecoration(level, chunk, structures);
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
}
finally { ExpansionOcean23.restoreDecoration(previous); }
}
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
@@ -49,7 +49,11 @@ public final class ExpansionRelief {
public static float sculpt(ExpansionIsland island, Column column, double y, float source) {
if (source <= 0) return source;
double reference = referenceY(island, y);
double canyon = 2.1 * column.canyonWeight() * smoothstep((reference - 120.0) / 34.0);
double canyonHeight = island.generation() == 24
? smoothstep((reference - 126 - column.uplift() * .35) / 16)
* smoothstep((226 + column.uplift() * .35 - reference) / 18)
: smoothstep((reference - 120.0) / 34.0);
double canyon = 2.1 * column.canyonWeight() * canyonHeight;
double crater = 2.1 * column.craterWeight() * smoothstep((reference - CRATER_FLOOR) / 14.0);
return (float) Math.clamp(source - Math.max(canyon, crater), -1.0, 1.0);
}
@@ -169,6 +169,15 @@ public final class ExpansionStructures23 {
if (centre.top < 32) return null;
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var resolver = view.getBiomeSource().createResolver(sampler);
var hutHeights = new ArrayList<Integer>();
// The biome is genuinely three dimensional: a swamp bank can be below another island floor.
for (int y = Math.min(centre.top + 1, 280); y >= 48 && hutHeights.size() < 4; y--) {
var floor = centre.column.getBlock(y - 1);
if ((!floor.isSolid() && !floor.getFluidState().is(net.minecraft.tags.FluidTags.WATER))
|| !centre.column.getBlock(y).isAir() || !centre.column.getBlock(y + 1).isAir()) continue;
if (!host.structureBiome(resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2)).is(net.minecraft.world.level.biome.Biomes.SWAMP)) continue;
if (hutHeights.isEmpty() || hutHeights.getLast() - y >= 8) hutHeights.add(y);
}
int undergroundY = Math.clamp(centre.top - 60, 50, 210);
Holder<Biome> surfaceBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, centre.top >> 2, z >> 2));
Holder<Biome> undergroundBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, undergroundY >> 2, z >> 2));
@@ -182,7 +191,8 @@ public final class ExpansionStructures23 {
if (holder == null) continue;
Holder<Biome> biome = entry.habitat() == Habitat.UNDERGROUND ? undergroundBiome
: entry.habitat() == Habitat.OCEAN ? waterBiome : surfaceBiome;
if (!holder.value().biomes().contains(biome)) continue;
if (entry.family().equals("swamp_hut")) { if (hutHeights.isEmpty()) continue; }
else if (!holder.value().biomes().contains(biome)) continue;
if (entry.habitat() == Habitat.OCEAN && !ExpansionOcean23.column(island, x, z).water()) continue;
long salt = mix(candidate.roll() ^ entry.id().hashCode() * 0x9e3779b97f4a7c15L);
double unit = ((salt >>> 11) + 1.0) * 0x1.0p-53;
@@ -196,10 +206,17 @@ public final class ExpansionStructures23 {
checkDeadline(deadlineNanos);
Structure original = choice.holder().value();
Entry entry = choice.entry();
int variants = entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
boolean hut = entry.family().equals("swamp_hut");
boolean layeredMine = island.origin() && island.generation() == 24 && entry.family().equals("mineshaft");
List<Integer> mineHeights = null;
int variants = hut ? hutHeights.size() : entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
for (int vertical = 0; vertical < variants; vertical++) {
if (mineHeights != null && vertical >= mineHeights.size()) break;
checkDeadline(deadlineNanos);
int targetY = undergroundY - vertical * 20;
int targetY = hut ? hutHeights.get(vertical) : undergroundY - vertical * 20;
ExpansionChunkGenerator nativeView = hut && view instanceof NativeView local
? local.atHeight(targetY + 1) : view;
GroundSampler nativeGround = hut ? new GroundSampler(nativeView, chunk, random, deadlineNanos, targetY + 1) : ground;
Structure generated = original;
if (entry.family().equals("trial_chambers")) generated = withStartHeight(original, registries, targetY);
Predicate<Holder<Biome>> valid = b -> original.biomes().contains(host.structureBiome(b));
@@ -207,18 +224,28 @@ public final class ExpansionStructures23 {
if (original instanceof StrongholdStructure) {
// Vanilla stronghold piece weights are static mutable state.
synchronized (StrongholdStructure.class) {
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
}
} else {
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
}
if (!start.isValid()) { diagnostic(island, candidate, "native-refused", entry.id()); continue; }
if (layeredMine) {
if (mineHeights == null) {
mineHeights = MineshaftHeights29.heights(start, undergroundY, ground.level.getMinY(), ground.level.getMaxY(),
(px, pz) -> ground.at(px, pz).column);
if (DIAGNOSTICS) SanctuaryMod.LOGGER.info("Native29 mine layers: seed {} chunk {} preferred {} chosen {} native pieces {}",
island.seed(), candidate.chunk(), undergroundY, mineHeights, start.getPieces().size());
}
if (vertical >= mineHeights.size()) break;
targetY = mineHeights.get(vertical);
}
// Only buried portal variants need the alternative underground elevations.
if (vertical > 0 && entry.family().equals("ruined_portal") && !undergroundPortal(start)) break;
start = adjustNativeStart(original, start, entry, island, targetY, ground);
String rejection = supportRejection(host, island, entry, start, ground);
start = adjustNativeStart(original, start, entry, island, targetY, nativeGround);
String rejection = supportRejection(host, island, entry, start, nativeGround);
if (rejection != null) { diagnostic(island, candidate, rejection, entry.id()); continue; }
((NativeStructureMetadata23) (Object) start).sanctuary$native23(new NativeStructureMetadata23.Data(island.seed(), seaLevel));
return new Accepted(choice.holder(), start);
@@ -308,19 +335,24 @@ public final class ExpansionStructures23 {
private static String supportRejection(ExpansionChunkGenerator host, ExpansionIsland island, Entry entry,
StructureStart start, GroundSampler ground) {
BoundingBox total = start.getBoundingBox();
boolean nativeMine = island.origin() && island.generation() == 24 && entry.family().equals("mineshaft");
List<BoundingBox> occupied = nativeMine ? MineshaftAdmission29.volumes(start, (x, z) -> ground.at(x, z).column) : null;
if (nativeMine && occupied == null) return "mineshaft-no-native-support";
if (island.origin() && entry.habitat() != Habitat.UNDERGROUND && !undergroundPortal(start)) {
int nearestX = Math.clamp(island.centerX(), total.minX(), total.maxX());
int nearestZ = Math.clamp(island.centerZ(), total.minZ(), total.maxZ());
if (Math.hypot((double) nearestX - island.centerX(), (double) nearestZ - island.centerZ()) < 56) return "natural-arrival-buffer";
}
if (total.minY() < ground.level.getMinY() + 3 || total.maxY() > ground.level.getMaxY() - 4) return "outside-height";
if (!inside(island, total.minX(), total.minZ(), 3) || !inside(island, total.maxX(), total.minZ(), 3)
|| !inside(island, total.minX(), total.maxZ(), 3) || !inside(island, total.maxX(), total.maxZ(), 3)) return "outside-island";
for (BoundingBox footprint : nativeMine ? occupied : List.of(total))
if (!inside(island, footprint.minX(), footprint.minZ(), 3) || !inside(island, footprint.maxX(), footprint.minZ(), 3)
|| !inside(island, footprint.minX(), footprint.maxZ(), 3) || !inside(island, footprint.maxX(), footprint.maxZ(), 3)) return "outside-island";
// Vanilla reference discovery scans eight chunks around the decorating chunk.
if (total.minX() < start.getChunkPos().getMinBlockX() - 128 || total.maxX() > start.getChunkPos().getMaxBlockX() + 128
|| total.minZ() < start.getChunkPos().getMinBlockZ() - 128 || total.maxZ() > start.getChunkPos().getMaxBlockZ() + 128) return "outside-native-reference-range";
if (host instanceof SanctuaryChunkGenerator city) {
if (city.lostCityPlan(island).intersects(total.minX(), total.minZ(), total.maxX(), total.maxZ())) return "occupied-city";
for (BoundingBox volume : nativeMine ? occupied : List.of(total))
if (city.lostCityPlan(island).intersects(volume.minX(), volume.minZ(), volume.maxX(), volume.maxZ())) return "occupied-city";
if (ground.localCity == null) {
var local = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
local.add(city.lostCityPlan(island), city.undergroundPlan16(island));
@@ -329,21 +361,54 @@ public final class ExpansionStructures23 {
if (reservation != null) local.addReservation(reservation);
ground.localCity = local.build();
}
if (ground.localCity.intersects(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsCityConstruction15(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsCityConstruction16(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsExperimentalStructures(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)) return "reserved-secret";
if (island.origin()) {
var legacyProtection = new BoundingBox(total.minX() - 2, total.minY() - 12, total.minZ() - 2,
total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2);
for (BoundingBox volume : nativeMine ? occupied : List.of(legacyProtection)) {
if (ground.localCity.intersects(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsCityConstruction15(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsCityConstruction16(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsExperimentalStructures(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ()))
return "reserved-secret";
}
if (island.origin() && !entry.family().equals("swamp_hut")) {
var binding = city.starterContext();
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
if (nativeMine) {
var regions = fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(binding.generator(), binding.random(),
total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16);
for (var volume : occupied) for (var region : regions)
if (MineshaftAdmission29.hydrology(region, volume)) return "protected-hydrology";
} else if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
}
}
if (entry.family().equals("swamp_hut")) {
if (host instanceof SanctuaryChunkGenerator sanctuary && island.origin()) {
var binding = sanctuary.starterContext();
var box = new BoundingBox(total.minX(), total.minY() - SwampHutPlacement28.MAX_PIER_DROP - 2,
total.minZ(), total.maxX(), total.maxY(), total.maxZ());
for (var region : fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(
binding.generator(), binding.random(), total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16)) {
String refusal = SwampHutPlacement28.hydrologyRejection(region, box);
if (refusal != null) return refusal;
}
}
return SwampHutPlacement28.rejection(start, (x, z) -> ground.at(x, z).column);
}
boolean underground = entry.habitat() == Habitat.UNDERGROUND || undergroundPortal(start);
boolean aquatic = entry.habitat() == Habitat.OCEAN;
int siteMin = Integer.MAX_VALUE, siteMax = Integer.MIN_VALUE;
for (StructurePiece piece : start.getPieces()) {
BoundingBox box = piece.getBoundingBox();
if (entry.family().equals("mineshaft")
&& piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
if (!nativeMine && !MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
continue;
}
if (nativeMine && piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCrossing) {
String crossingRefusal = MineshaftCrossing29.rejection(piece, start.getPieces(), (x, z) -> ground.at(x, z).column, ground.level.getMaxY());
if (crossingRefusal != null) return crossingRefusal;
continue;
}
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE, supported = 0, sampled = 0;
boolean buriedPiece = underground || !aquatic && box.maxY() < ground.at(box.getCenter().getX(), box.getCenter().getZ()).top - 6;
int step = buriedPiece ? 8 : 4;
@@ -372,7 +437,12 @@ public final class ExpansionStructures23 {
low = Math.min(low, column.top); high = Math.max(high, column.top);
if (high - low > (entry.family().endsWith("pyramid") ? 5 : 9)) return "surface-slope";
}
if (buriedPiece && supported < Math.ceil(sampled * (entry.family().equals("mineshaft") ? 0.65 : 0.85))) return "cavity-not-enclosed";
if (buriedPiece && supported < Math.ceil(sampled * (entry.family().equals("mineshaft") ? 0.65 : 0.85))) {
if (nativeMine && DIAGNOSTICS) SanctuaryMod.LOGGER.info(
"Native29 mine enclosure: seed {} chunk {} piece {} box {} supported {}/{} required {}",
island.seed(), start.getChunkPos(), piece.getClass().getSimpleName(), box, supported, sampled, (int) Math.ceil(sampled * 0.65));
return "cavity-not-enclosed";
}
if (!buriedPiece && !aquatic) {
siteMin = Math.min(siteMin, low); siteMax = Math.max(siteMax, high);
if (siteMax - siteMin > 20) return "site-slope";
@@ -474,7 +544,7 @@ public final class ExpansionStructures23 {
}
private static int weight(String family) {
return switch (family) { case "camp" -> 6; case "ruined_portal", "mineshaft", "ocean_ruin" -> 3;
case "village", "shipwreck", "trail_ruins", "buried_treasure" -> 2; default -> 1; };
case "village", "shipwreck", "trail_ruins", "buried_treasure", "swamp_hut" -> 2; default -> 1; };
}
private static int[] samples(int low, int high, int step) {
var values = new java.util.TreeSet<Integer>(); values.add(low); values.add(high); values.add((low + high) / 2);
@@ -493,9 +563,11 @@ public final class ExpansionStructures23 {
private record Choice(Entry entry, Holder<Structure> holder, double score) {}
private static final class Ground {
final NoiseColumn column; final int top; final int thickness;
Ground(NoiseColumn column, int min, int max) {
Ground(NoiseColumn column, int min, int max) { this(column, min, max, false); }
Ground(NoiseColumn column, int min, int max, boolean waterSurface) {
this.column = column; int y = max;
while (y >= min && !column.getBlock(y).isSolid()) y--;
while (y >= min && !column.getBlock(y).isSolid()
&& !(waterSurface && column.getBlock(y).getFluidState().is(net.minecraft.tags.FluidTags.WATER))) y--;
top = y;
while (y >= min && top - y < 24 && solid(y)) y--;
thickness = top - y;
@@ -505,17 +577,23 @@ public final class ExpansionStructures23 {
}
private static final class GroundSampler {
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
final Integer localCeiling;
final Map<Long, Ground> columns = new HashMap<>();
fr.koka.sanctuary.worldgen.city.LostCityProtection16 localCity;
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline) {
this(generator, level, random, deadline, null);
}
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline, Integer localCeiling) {
this.generator = generator; this.level = level; this.random = random; this.deadline = deadline;
this.localCeiling = localCeiling;
}
Ground at(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
Ground old = columns.get(key); if (old != null) return old;
checkDeadline(deadline);
if (columns.size() >= MAX_COLUMN_SAMPLES) throw new SampleBudgetExceeded();
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), level.getMaxY());
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(),
localCeiling == null ? level.getMaxY() : localCeiling, localCeiling != null);
columns.put(key, result); return result;
}
}
@@ -536,14 +614,23 @@ public final class ExpansionStructures23 {
/** Isolated biome snapshot during preflight, per-island sea level during native generation. */
private static final class NativeView extends ExpansionChunkGenerator {
private final ExpansionChunkGenerator host; private final ExpansionIsland island;
private Integer localCeiling;
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
}
NativeView atHeight(int ceiling) {
var result = new NativeView(host, island, source(), registries); result.localCeiling = ceiling; return result;
}
@Override public int rootGeneration() { return island.generation(); }
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
if (localCeiling != null) {
var column = getBaseColumn(x, z, level, random);
for (int y = localCeiling; y >= level.getMinY(); y--) if (type.isOpaque().test(column.getBlock(y))) return y + 1;
return level.getMinY();
}
return island.origin() ? host.getBaseHeight(x, z, type, level, random) : super.getBaseHeight(x, z, type, level, random);
}
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Occupied native volumes, including potential wooden feet and suspension chains. No connecting-room infill. */
public final class MineshaftAdmission29 {
private MineshaftAdmission29() {}
public static List<BoundingBox> volumes(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
var result = new ArrayList<BoundingBox>();
for (var piece : start.getPieces()) {
var box = piece.getBoundingBox();
result.add(body(box));
if (piece instanceof MineshaftPieces.MineShaftCorridor) {
var anchors = MineshaftSupport28.anchorVolumes(piece, columns);
if (anchors == null) return null;
result.addAll(anchors);
}
}
return List.copyOf(result);
}
public static BoundingBox body(BoundingBox box) {
return new BoundingBox(box.minX() - 2, box.minY() - 1, box.minZ() - 2,
box.maxX() + 2, box.maxY() + 2, box.maxZ() + 2);
}
public static boolean intersects(List<BoundingBox> volumes, BoundingBox other) {
return volumes.stream().anyMatch(box -> box.intersects(other));
}
/** Same protected beds, sources and flow margins as SanctuaryDecorationProtection, queried as a rectangle. */
public static boolean hydrology(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
var water = region.water(); var lava = region.lava();
for (int x = box.minX() - 1; x <= box.maxX() + 1; x++) for (int z = box.minZ() - 1; z <= box.maxZ() + 1; z++)
for (var cell : water.cellsAt(x, z))
if (box.minY() <= cell.carveTop() + 1 && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 2) return true;
for (var spring : water.springs()) {
var b = spring.flowBounds();
if (intersects(box, b.minX()-2,b.minY()-2,b.minZ()-2,b.maxX()+2,b.maxY()+2,b.maxZ()+2)) return true;
var p = spring.source();
if (intersects(box,p.x()-3,p.y()-3,p.z()-3,p.x()+3,p.y()+3,p.z()+3)) return true;
}
for (var b : lava.flowBounds())
if (intersects(box,b.minX()-8,b.minY()-8,b.minZ()-8,b.maxX()+8,b.maxY()+8,b.maxZ()+8)) return true;
for (var spill : water.spills()) {
var b = spill.flowBounds();
if (intersects(box,b.minX()-3,b.minY()-3,b.minZ()-3,b.maxX()+3,b.maxY()+3,b.maxZ()+3)) return true;
}
for (var p : lava.cells()) if (intersects(box,p.x()-2,p.y()-2,p.z()-2,p.x()+2,p.y()+2,p.z()+2)) return true;
for (var p : lava.supports()) if (intersects(box,p.x()-2,p.y()-2,p.z()-2,p.x()+2,p.y()+2,p.z()+2)) return true;
return false;
}
private static boolean intersects(BoundingBox b, int minX,int minY,int minZ,int maxX,int maxY,int maxZ) {
return b.minX()<=maxX && b.maxX()>=minX && b.minY()<=maxY && b.maxY()>=minY && b.minZ()<=maxZ && b.maxZ()>=minZ;
}
}
@@ -0,0 +1,51 @@
package fr.koka.sanctuary.expansion;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Native crossings have a plank floor; their optional interior pillars do not require a close rock roof. */
public final class MineshaftCrossing29 {
private MineshaftCrossing29() {}
public static String rejection(StructurePiece crossing, List<StructurePiece> pieces,
BiFunction<Integer, Integer, NoiseColumn> columns, int maxY) {
var box = crossing.getBoundingBox();
boolean needsCarriedConnection = false;
int floorY = box.minY() - 1;
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
var column = columns.apply(x, z); var floor = column.getBlock(floorY);
if (!floor.getFluidState().isEmpty()) return "mineshaft-crossing-wet-floor";
if (floor.isFaceSturdy(EmptyBlockGetter.INSTANCE, new BlockPos(x, floorY, z), Direction.UP)) continue;
needsCarriedConnection = true;
// StructurePiece.isInterior tests floorY+1 against OCEAN_FLOOR_WG. Only count
// opaque terrain above the crossing's excavated body, so its own carving cannot remove it.
boolean interior = false;
for (int y = box.maxY() + 1; y < maxY; y++) if (Heightmap.Types.OCEAN_FLOOR_WG.isOpaque().test(column.getBlock(y))) {
interior = true; break;
}
if (!interior) return "mineshaft-crossing-unplaceable-floor";
}
// A wholly natural floor already carries itself. A new plank bridge borrows an actual
// adjacent corridor's certified support, never a distant or merely overlapping box.
if (needsCarriedConnection && pieces.stream().noneMatch(piece -> piece instanceof MineshaftPieces.MineShaftCorridor
&& joins(box, piece) && MineshaftSupport28.anchorVolumes(piece, columns) != null))
return "mineshaft-crossing-no-carried-connection";
return null;
}
private static boolean joins(BoundingBox crossing, StructurePiece corridor) {
var box = corridor.getBoundingBox();
if (box.minY() != crossing.minY()) return false;
if (corridor.getOrientation().getAxis() == Direction.Axis.X)
return (box.maxX() + 1 == crossing.minX() || crossing.maxX() + 1 == box.minX())
&& Math.min(box.maxZ(), crossing.maxZ()) - Math.max(box.minZ(), crossing.minZ()) + 1 >= 3;
return (box.maxZ() + 1 == crossing.minZ() || crossing.maxZ() + 1 == box.minZ())
&& Math.min(box.maxX(), crossing.maxX()) - Math.max(box.minX(), crossing.minX()) + 1 >= 3;
}
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.expansion;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Coarse vertical search in the actual native graph, before unchanged detailed admission. */
public final class MineshaftHeights29 {
public static final int MAX_COLUMNS = 32;
public record Probe(NoiseColumn column, int floorOffset, int roofOffset, boolean corridor) {}
private record Score(int y, int covered, int direct, int distance) {}
private MineshaftHeights29() {}
public static List<Integer> heights(StructureStart start, int preferred, int minY, int maxY,
BiFunction<Integer, Integer, NoiseColumn> columns) {
var pieces = start.getPieces(); var total = start.getBoundingBox();
int originY = (total.minY() + total.maxY()) / 2;
int count = Math.min(MAX_COLUMNS, pieces.size());
var probes = new ArrayList<Probe>(count);
for (int i = 0; i < count; i++) {
var piece = pieces.get(count == 1 ? 0 : i * (pieces.size() - 1) / (count - 1));
var box = piece.getBoundingBox(); var center = box.getCenter();
probes.add(new Probe(columns.apply(center.getX(), center.getZ()), box.minY() - 1 - originY,
box.maxY() + 1 - originY, piece instanceof MineshaftPieces.MineShaftCorridor));
}
return choose(probes, preferred, minY + 3 + originY - total.minY(), maxY - 4 - total.maxY() + originY);
}
/** At most96 sampled levels in a384-block world; three native trials, not three new graphs per level. */
public static List<Integer> choose(List<Probe> probes, int preferred, int minY, int maxY) {
var scores = new ArrayList<Score>();
for (int y = Math.floorDiv(minY + 3, 4) * 4; y <= maxY; y += 4) {
int covered = 0, direct = 0;
for (var probe : probes) {
int floor = y + probe.floorOffset(), roof = y + probe.roofOffset();
boolean floorMass = solid(probe.column(), floor), roofMass = solid(probe.column(), roof);
boolean supported = probe.corridor()
? floorMass || anchor(probe.column(), floor - 1, floor - 20, -1)
|| anchor(probe.column(), roof, floor + 50, 1)
: near(probe.column(), floor, 6) && near(probe.column(), roof, 6);
if (supported) covered++;
if (floorMass) direct++;
if (roofMass) direct++;
}
if (covered > 0) scores.add(new Score(y, covered, direct, Math.abs(y - preferred)));
}
scores.sort(Comparator.comparingInt(Score::covered).reversed()
.thenComparing(Comparator.comparingInt(Score::direct).reversed())
.thenComparingInt(Score::distance).thenComparingInt(Score::y));
var chosen = new ArrayList<Integer>(3);
for (var score : scores) {
if (chosen.stream().anyMatch(y -> Math.abs(y - score.y()) < 8)) continue;
chosen.add(score.y()); if (chosen.size() == 3) break;
}
return List.copyOf(chosen);
}
private static boolean solid(NoiseColumn column, int y) {
var state = column.getBlock(y); return state.isSolid() && state.getFluidState().isEmpty();
}
private static boolean near(NoiseColumn column, int y, int reach) {
for (int offset = -reach; offset <= reach; offset++) if (solid(column, y + offset)) return true;
return false;
}
private static boolean anchor(NoiseColumn column, int from, int to, int step) {
for (int y = from; step > 0 ? y <= to : y >= to; y += step) {
if (!column.getBlock(y).getFluidState().isEmpty()) return false;
if (solid(column, y)) return true;
}
return false;
}
}
@@ -0,0 +1,74 @@
package fr.koka.sanctuary.expansion;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
/** Certifies the support positions actually used by vanilla MineShaftCorridor, not a percentage of nearby rock. */
public final class MineshaftSupport28 {
private MineshaftSupport28() {}
public static boolean anchored(StructurePiece piece, BiFunction<Integer, Integer, NoiseColumn> columns) {
return anchorVolumes(piece, columns, false) != null;
}
/** Protect the nearer usable native anchor; downward wins an equal-distance tie. */
public static java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> anchorVolumes(StructurePiece piece,
BiFunction<Integer, Integer, NoiseColumn> columns) {
return anchorVolumes(piece, columns, true);
}
private static java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> anchorVolumes(StructurePiece piece,
BiFunction<Integer, Integer, NoiseColumn> columns, boolean allowDirectFloor) {
var volumes = new java.util.ArrayList<net.minecraft.world.level.levelgen.structure.BoundingBox>();
var box = piece.getBoundingBox();
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? box.getZSpan() : box.getXSpan();
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) {
int sectionAnchors = volumes.size();
for (int x : new int[]{0, 2}) {
var pier = SwampHutPlacement28.worldPosition(piece, x, -1, z);
var column = columns.apply(pier.getX(), pier.getZ());
var floor = column.getBlock(pier.getY());
// Vanilla setPlanksBlock retains a sturdy natural floor. Only a floor plank subsequently
// invokes placeDoubleLowerOrUpperSupport, so a rock ledge needs no distant anchor.
if (allowDirectFloor && floor.getFluidState().isEmpty()
&& !floor.is(net.minecraft.world.level.block.Blocks.OAK_PLANKS)
&& !floor.is(net.minecraft.world.level.block.Blocks.DARK_OAK_PLANKS)
&& floor.isFaceSturdy(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, pier, net.minecraft.core.Direction.UP)) {
volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier));
continue;
}
Integer below = null, above = null;
// Native wooden support searches at most20 below its floor plank.
for (int y = pier.getY() - 1; y >= pier.getY() - 20; y--) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) {
below = y; break;
}
}
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) {
above = y; break;
}
}
if (below == null && above == null) {
if (!allowDirectFloor) return null;
// Vanilla simply omits this optional pillar/chain. The other side of the same
// transverse section can still carry the corridor; both ends must remain anchored.
continue;
}
if (below != null && (above == null || pier.getY() - below <= above - pier.getY()))
volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier.getX(), below, pier.getZ(), pier.getX(), pier.getY(), pier.getZ()));
else volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier.getX(), pier.getY(), pier.getZ(), pier.getX(), above, pier.getZ()));
}
if (volumes.size() == sectionAnchors) {
if (allowDirectFloor && Boolean.getBoolean("sanctuary.expansion.structureDiagnostics"))
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("Native29 mine unanchored section: piece {} facing {} localZ {} floorY {}",
box, piece.getOrientation(), z, box.minY() - 1);
return null;
}
}
return java.util.List.copyOf(volumes);
}
}
@@ -7,10 +7,34 @@ import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPiec
/** Scoped native placement parameters; never change the sea level of another dimension or start. */
public final class NativeStructurePlacement23 {
private static final ThreadLocal<Integer> SEA_LEVEL = new ThreadLocal<>();
public record HutScope(java.util.List<net.minecraft.core.BlockPos> piers,
java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> bodies) {}
private static final ThreadLocal<HutScope> HUT = new ThreadLocal<>();
private NativeStructurePlacement23() {}
public static Integer seaLevel() { return SEA_LEVEL.get(); }
public static Integer enter(int seaLevel) { Integer previous = SEA_LEVEL.get(); SEA_LEVEL.set(seaLevel); return previous; }
public static void restore(Integer previous) { if (previous == null) SEA_LEVEL.remove(); else SEA_LEVEL.set(previous); }
public static HutScope enterHut(net.minecraft.world.level.levelgen.structure.StructureStart start) {
var previous = HUT.get();
var pieces = start.getPieces().stream().filter(net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece.class::isInstance).toList();
HUT.set(new HutScope(pieces.stream().flatMap(piece -> SwampHutPlacement28.piers(piece).stream()).toList(),
pieces.stream().map(StructurePiece::getBoundingBox).toList()));
return previous;
}
public static void restoreHut(HutScope previous) {
if (previous == null) HUT.remove(); else HUT.set(previous);
}
public static boolean allowsHutWrite(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state,
net.minecraft.world.level.block.state.BlockState replaced) {
var scope = HUT.get(); if (scope == null) return false;
// Native construction in already empty air above a certified bank does not cut its retaining bed.
if (replaced.isAir() && scope.bodies().stream().anyMatch(box -> box.isInside(pos))) return true;
if (!state.is(net.minecraft.world.level.block.Blocks.OAK_LOG)
|| !(replaced.isAir() || replaced.getFluidState().isSource()
&& replaced.getFluidState().is(net.minecraft.tags.FluidTags.WATER))) return false;
return scope.piers().stream().anyMatch(pier -> pier.getX() == pos.getX() && pier.getZ() == pos.getZ()
&& pos.getY() <= pier.getY() && pos.getY() > pier.getY() - SwampHutPlacement28.MAX_PIER_DROP);
}
public static void move(StructurePiece piece, int dy) {
if (piece instanceof OceanMonumentPieces.MonumentBuilding monument) {
for (StructurePiece child : ((MonumentChildren23Accessor) monument).sanctuary$children()) child.move(0, dy, 0);
@@ -0,0 +1,87 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.worldgen.PopulationHydrology;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.material.Fluids;
/** Native hut geometry on a real shallow bank, including banks below another island floor. */
public final class SwampHutPlacement28 {
public static final int MAX_PIER_DROP = 8;
private SwampHutPlacement28() {}
public static String rejection(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
for (var piece : start.getPieces()) {
if (!(piece instanceof SwampHutPiece)) return "hut-unexpected-piece";
var box = piece.getBoundingBox();
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
var column = columns.apply(x, z);
int waterTop = Integer.MIN_VALUE, waterBottom = Integer.MAX_VALUE;
for (int y = box.minY() - MAX_PIER_DROP; y <= box.maxY(); y++) {
var state = column.getBlock(y); var fluid = state.getFluidState();
if (y >= box.minY() + 2 && !state.isAir()) return "hut-natural-headroom";
if (fluid.isEmpty()) continue;
if (fluid.getType() != Fluids.WATER || !fluid.isSource()) return "hut-flowing-or-nonwater";
waterTop = Math.max(waterTop, y); waterBottom = Math.min(waterBottom, y);
}
if (waterTop != Integer.MIN_VALUE) {
if (waterTop > box.minY() || waterTop - waterBottom + 1 > 4) return "hut-deep-water";
if (!solid(column.getBlock(waterBottom - 1)) || !solid(column.getBlock(waterBottom - 2))) return "hut-unsealed-bed";
}
}
for (var pier : piers(piece)) {
var column = columns.apply(pier.getX(), pier.getZ()); int y = pier.getY(), depth = 0;
while (replaceable(column.getBlock(y)) && depth <= MAX_PIER_DROP) { y--; depth++; }
if (depth > MAX_PIER_DROP || !solid(column.getBlock(y)) || !solid(column.getBlock(y - 1))) return "hut-unsupported-pier";
}
}
return null;
}
/** Still water is compatible; springs, spills, channels and lava keep their protection. */
public static String hydrologyRejection(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
for (var spring : region.water().springs()) if (intersects(box, spring.flowBounds(), 3)) return "hut-protected-spring";
for (var spill : region.water().spills()) if (intersects(box, spill.flowBounds(), 3)) return "hut-protected-spill";
for (var flow : region.lava().flowBounds()) if (intersects(box, flow, 4)) return "hut-protected-lava";
var rivers = region.water().features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.RIVER)
.map(PopulationHydrology.Feature::id).collect(java.util.stream.Collectors.toSet());
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
for (var cell : region.water().cellsAt(x, z)) if (rivers.contains(cell.featureId())
&& box.minY() <= cell.carveTop() && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 1) return "hut-protected-river";
return null;
}
/** Exact four support origins used by SwampHutPiece.postProcess. */
public static List<BlockPos> piers(StructurePiece piece) {
var result = new ArrayList<BlockPos>(4);
for (int z : new int[]{2, 7}) for (int x : new int[]{1, 5}) result.add(worldPosition(piece, x, -1, z));
return List.copyOf(result);
}
public static BlockPos worldPosition(StructurePiece piece, int x, int y, int z) {
var b = piece.getBoundingBox(); Direction facing = piece.getOrientation();
if (facing == null || facing.getAxis().isVertical()) throw new IllegalArgumentException("A hut requires horizontal orientation");
int wx = switch (facing) { case WEST -> b.maxX() - z; case EAST -> b.minX() + z; default -> b.minX() + x; };
int wz = switch (facing) { case NORTH -> b.maxZ() - z; case SOUTH -> b.minZ() + z; default -> b.minZ() + x; };
return new BlockPos(wx, b.minY() + y, wz);
}
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
private static boolean replaceable(BlockState state) {
return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS);
}
private static boolean intersects(BoundingBox b, PopulationHydrology.FlowBounds flow, int margin) {
return b.minX() <= flow.maxX() + margin && b.maxX() >= flow.minX() - margin
&& b.minY() <= flow.maxY() + margin && b.maxY() >= flow.minY() - margin
&& b.minZ() <= flow.maxZ() + margin && b.maxZ() >= flow.minZ() - margin;
}
}
@@ -67,8 +67,9 @@ public abstract class NativeStructureStart23Mixin implements NativeStructureMeta
RandomSource random, BoundingBox bounds, ChunkPos pos, Operation<Void> original) {
if (sanctuary$native23 == null) { original.call(level, structures, generator, random, bounds, pos); return; }
Integer previous = NativeStructurePlacement23.enter(sanctuary$native23.seaLevel());
var previousHut = NativeStructurePlacement23.enterHut((StructureStart) (Object) this);
try { original.call(level, structures, generator, random, bounds, pos); }
finally { NativeStructurePlacement23.restore(previous); }
finally { NativeStructurePlacement23.restoreHut(previousHut); NativeStructurePlacement23.restore(previous); }
}
@Unique private static Data sanctuary$read(CompoundTag tag) {
@@ -62,6 +62,10 @@ public abstract class UnifiedDecorationProtectionMixin {
}
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
if (state.is(Blocks.SUGAR_CANE)) return;
// Admitted native huts use existing air and four bounded wooden piers in still water.
// The intact basin bed, all other water cells, island ownership and secrets remain protected.
if (!generator.protectsCityDecoration(pos)
&& fr.koka.sanctuary.expansion.NativeStructurePlacement23.allowsHutWrite(pos, state, region.getBlockState(pos))) return;
// Generation16 reserves its architectural volume before decoration. Only its own
// native pieces may replace water there; published generations retain the old rule.
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.worldgen;
/** Bounded domain deformation around native rock noise, followed by native cave cuts. */
public final class CavesRelief28 {
public static final int CAVE_OFFSET = 96;
public static final int DEFORMATION_START = 180;
public static final double MAX_HORIZONTAL_SHIFT = 16;
public static final double MAX_VERTICAL_SHIFT = 40;
private CavesRelief28() {}
public record Warp(double x, double y, double z) {}
public static double deformationWeight(int x, int y, int z, double radius) {
double distance = Math.hypot((double) x, z);
double arrival = smooth((distance - 48) / 48);
double coast = 1 - smooth((distance / radius - .66) / .26);
return smooth((y - DEFORMATION_START) / 48.0) * arrival * coast;
}
/** Signed smooth displacements: no folded ridge, plateau saturation or height-map uplift. */
public static Warp warp(double weight, float across, float vertical, float along) {
return new Warp(MAX_HORIZONTAL_SHIFT * Math.tanh(across * 1.6) * weight,
MAX_VERTICAL_SHIFT * Math.tanh(vertical * 1.6) * weight,
MAX_HORIZONTAL_SHIFT * Math.tanh(along * 1.6) * weight);
}
public static float carve(float rock, float nativeCaves, int y) {
if (rock <= 0) return rock;
double weight = smooth((y - 40) / 28.0) * (1 - smooth((y - 202) / 30.0));
if (weight <= 0) return rock;
return Math.min(rock, (float) Math.clamp(nativeCaves + (1 - weight) * 2, -1, 1));
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import java.util.ArrayList;
import java.util.Comparator;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
/** A few attempts at real cave walls; the vanilla feature owns its shell, loot and spawner. */
public final class CavityMonsterRoom28 {
private static final MonsterRoomFeature NATIVE = new MonsterRoomFeature();
private static final int MAX_ATTEMPTS = 8;
private CavityMonsterRoom28() {}
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
if (!(generator instanceof ExpansionChunkGenerator host) || host.rootGeneration() != 24
|| !level.getLevel().structureManager().shouldGenerateStructures()) return;
var pos = chunk.getPos(); var island = host.islandForChunk(pos.x(), pos.z());
if (island == null || island.generation() != 24) return;
long seed = WoodlandGroveGeometry.seed(island.seed(), pos.x(), 28, pos.z(), 0xD086E028L);
if (Math.floorMod(seed, 3) != 0) return;
var candidates = new ArrayList<BlockPos>();
int bottom = Math.max(level.getMinY() + 2, 32), top = Math.min(level.getMaxY() - 6, 280);
for (int dx : new int[]{4, 11}) for (int dz : new int[]{4, 11}) {
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
for (int y = bottom; y <= top; y++) {
BlockPos at = new BlockPos(x, y, z);
if (!level.getBlockState(at.below()).isSolid() || !level.getBlockState(at.above(4)).isSolid()) continue;
boolean caveEdge = false;
for (Direction side : Direction.Plane.HORIZONTAL) {
BlockPos opening = at.relative(side, 4);
if (level.getBlockState(opening).isAir() && level.getBlockState(opening.above()).isAir()) { caveEdge = true; break; }
}
if (caveEdge) candidates.add(at);
}
}
candidates.sort(Comparator.<BlockPos>comparingLong(at -> WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 28)));
int attempts = 0;
for (BlockPos at : candidates) {
if (attempts++ >= MAX_ATTEMPTS) break;
if (!safe(level, host, at, island.id())) continue;
// No virtual solid floor/roof and no prebuilt room. Native rejection remains authoritative.
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) return;
}
}
private static boolean safe(WorldGenLevel level, ExpansionChunkGenerator host, BlockPos at, String owner) {
for (int dx = -4; dx <= 4; dx++) for (int dz = -4; dz <= 4; dz++) for (int dy = -1; dy <= 4; dy++) {
BlockPos block = at.offset(dx, dy, dz);
var destination = host.islandForChunk(block.getX() >> 4, block.getZ() >> 4);
if (destination == null || !destination.id().equals(owner)) return false;
var state = level.getBlockState(block);
if (!state.getFluidState().isEmpty() || state.hasBlockEntity() || state.is(Blocks.BEDROCK)) return false;
if (host instanceof SanctuaryChunkGenerator sanctuary
&& (sanctuary.protectsDecoration(block) || sanctuary.protectsCityDecoration(block))) return false;
}
return true;
}
}
@@ -0,0 +1,53 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkAccess;
/** Native finite ore veins, positioned within the whole floating rock column rather than vanilla sea-level bands. */
public final class IslandOres28 {
private IslandOres28() {}
private record Deposit(String feature, int attempts, double minDepth, double maxDepth) {}
// Small supplementary populations shared by all climates. Existing biome ores remain in place.
private static final Deposit[] DEPOSITS = {
new Deposit("ore_coal", 8, .05, .65), new Deposit("ore_iron", 7, .12, .88),
new Deposit("ore_copper_small", 6, .10, .66), new Deposit("ore_gold", 2, .52, .90),
new Deposit("ore_redstone", 3, .56, .92), new Deposit("ore_lapis", 2, .30, .82),
new Deposit("ore_diamond_small", 2, .70, .94), new Deposit("ore_emerald", 1, .25, .85)
};
public static void place(WorldGenLevel level, ExpansionChunkGenerator generator, ChunkAccess chunk, ExpansionIsland island) {
if (island == null || generator.rootGeneration() != 24) return;
int minX = chunk.getPos().getMinBlockX(), minZ = chunk.getPos().getMinBlockZ();
// Density columns precede structures and vegetation. Cavities do not reset mineral depth:
// retain the full top-to-bottom envelope rather than using the nearest cave ceiling.
var column = generator.getBaseColumn(minX + 8, minZ + 8, level, generator.randomState(island));
int top = -1, bottom = -1;
for (int y = 1; y < 383; y++) if (column.getBlock(y).is(BlockTags.BASE_STONE_OVERWORLD)) {
if (bottom < 0) bottom = y;
top = y;
}
if (top - bottom < 24) return;
var registry = level.registryAccess().lookupOrThrow(Registries.FEATURE);
for (int index = 0; index < DEPOSITS.length; index++) {
Deposit deposit = DEPOSITS[index];
var feature = registry.getValue(Identifier.withDefaultNamespace(deposit.feature()));
if (feature == null) throw new IllegalStateException("Missing native ore " + deposit.feature());
var random = RandomSource.create(WoodlandGroveGeometry.seed(island.seed(), minX, index, minZ, 0x0280AE5L));
for (int attempt = 0; attempt < deposit.attempts(); attempt++) {
double depth = deposit.minDepth() + random.nextDouble() * (deposit.maxDepth() - deposit.minDepth());
int y = (int) Math.round(top - depth * (top - bottom));
// Largest selected native vein is 17 blocks (radius bound five). Its writes stay
// in the decorating chunk, including where neighbours are generated in another order.
var at = new BlockPos(minX + 5 + random.nextInt(6), y, minZ + 5 + random.nextInt(6));
if (level.getBlockState(at).is(BlockTags.BASE_STONE_OVERWORLD)) feature.place(level, generator, random, at);
}
}
}
}
@@ -31,7 +31,7 @@ public final class PopulationHydrology {
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER, TERRACE }
public enum Kind { POND, LAKE, RIVER, TERRACE, OUTLET }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
@@ -133,6 +133,23 @@ public final class PopulationHydrology {
public List<Feature> features() { return features; }
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
public boolean isOutletCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.OUTLET && f.id() == cell.featureId()); }
/** Add an island-wide channel or short bank cuts, retaining the deeper water terraces. */
public Plan overlay30(Plan other) {
var surface = new LinkedHashMap<Long, Cell>();
var lowerCells = new ArrayList<Cell>();
for (var cell : allCells) {
if (isTerraceCell(cell)) lowerCells.add(cell);
else surface.put(key(cell.x(), cell.z()), cell);
}
for (var cell : other.allCells) surface.put(key(cell.x(), cell.z()), cell);
var mergedFeatures = new LinkedHashMap<Long, Feature>();
features.forEach(f -> mergedFeatures.put(f.id(), f));
other.features.forEach(f -> mergedFeatures.put(f.id(), f));
var mergedSpills = new ArrayList<>(spills); mergedSpills.addAll(other.spills);
var mergedCells = new ArrayList<>(surface.values()); mergedCells.addAll(lowerCells);
return rebuild(mergedCells, List.copyOf(mergedFeatures.values()), springs, springFlow, terraces, mergedSpills);
}
public List<Terrace> terraces() { return terraces; }
public List<Spill> spills() { return spills; }
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
@@ -254,6 +271,10 @@ public final class PopulationHydrology {
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
static Plan additional30(Map<Long, Cell> cells, List<Feature> features, List<Spill> spills, int columns, int samples) {
return new Plan(cells, features, List.of(), Set.of(), columns, samples,
new PopulationTerraces.Result(List.of(), List.of(), List.of(), spills, Set.of()));
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
@@ -17,6 +17,25 @@ public final class PopulationHydrologyRuntime {
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, PopulationHydrology.Plan> RIVERS30 = new WeakHashMap<>();
public static boolean current30(NoiseBasedChunkGenerator generator) {
var settings = generator.generatorSettings();
return settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"));
}
public static PopulationHydrology.Plan river30(NoiseBasedChunkGenerator generator, RandomState state) {
synchronized (RIVERS30) {
return RIVERS30.computeIfAbsent(state, ignored -> {
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
var plan = River30.create(state.seed(), (int) capacity(generator).radius(),
new SignMemo((x,y,z) -> sampler.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)));
SanctuaryMod.LOGGER.info("River30 seed {}: {} cells, {} outlets, {} path points", state.seed(),
plan.cells().size(), plan.spills().size(), plan.features().stream().mapToInt(f -> f.path().size()).sum());
return plan;
});
}
}
private PopulationHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
@@ -47,6 +66,10 @@ public final class PopulationHydrologyRuntime {
});
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
var water = PopulationHydrology.create(seed, memo).admitted();
if (current30(generator)) {
water = River30.openBanks(seed, water, memo, 1)
.overlay30(river30(generator, state).translated(-region.originX(), -region.originZ()));
}
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
lava.translated(region.originX(), region.originZ()));
@@ -133,6 +156,7 @@ public final class PopulationHydrologyRuntime {
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
if (plan.isOutletCell(cell)) continue;
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
@@ -1,6 +1,7 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.concurrent.ConcurrentHashMap;
@@ -10,7 +11,9 @@ import net.minecraft.world.level.levelgen.densityfunction.*;
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
boolean terraces29, boolean relief30) implements DensityFunction {
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
@@ -19,36 +22,106 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction.CODEC.fieldOf("underside").forGetter(PopulationIslandDensity::underside),
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(PopulationIslandDensity::riftWarp),
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(PopulationIslandDensity::riftDetail),
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity)
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity),
DensityFunction.CODEC.optionalFieldOf("caves_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::caves28),
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28),
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29),
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30)
).apply(instance, PopulationIslandDensity::new));
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false);
}
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
caves28, ridges28, erosion28, geology28, false, false);
}
@Override public DensitySampler compileSampler(CompileContext context) {
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
var fractureDetail = riftDetail.compileSampler(context);
var caves = geology28 ? caves28.compileSampler(context) : null;
var ridges = geology28 ? ridges28.compileSampler(context) : null;
var erosion = geology28 ? erosion28.compileSampler(context) : null;
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
return new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) {
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
double fx = x - ix, fy = y - iy, fz = z - iz;
double sum = 0;
// Trilinear sampling avoids integer snapping of the displaced native field.
for (int dz = 0; dz <= (fz == 0 ? 0 : 1); dz++)
for (int dy = 0; dy <= (fy == 0 ? 0 : 1); dy++)
for (int dx = 0; dx <= (fx == 0 ? 0 : 1); dx++) {
double w = (dx == 0 ? 1 - fx : fx) * (dy == 0 ? 1 - fy : fy)
* (dz == 0 ? 1 - fz : fz);
sum += w * raw.sampleValue(sample, ix + dx, iy + dy, iz + dz);
}
return (float) sum;
}
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
int localY = y - 64;
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
float coarseValue = coarse.sampleValue(sample, x, localY, z);
float fineValue = fine.sampleValue(sample, x, localY, z);
float terrainValue;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
if (weight > 0) {
var shift = CavesRelief28.warp(weight,
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
ridges.sampleValue(sample, x, y, z),
erosion.sampleValue(sample, x - 211, y + 137, z + 307));
double sourceY = y - 64 - shift.y();
if (terraces29) sourceY = Terraces29.sourceY(sourceY, coarseValue, fineValue,
Terraces29.weight(y, weight));
terrainValue = terrainAt(sample, x - shift.x(), sourceY, z - shift.z());
} else terrainValue = raw.sampleValue(sample, x, y - 64, z);
// Keep the island envelope at actual coordinates, independent from displaced rock.
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
edge.sampleValue(sample, x, 0, z), coarseValue,
fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
if (natural <= 0) return natural;
var region = capacity.regionAt(x, z);
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
int rx = x - region.originX(), rz = z - region.originZ();
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
return RiftShape.density(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
boolean affects = geology28 ? RiftShape.mayAffect28(layout, rx, rz) : RiftShape.mayAffect(layout, rx, rz);
if (affects) natural = geology28
? RiftShape.density28(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z))
: RiftShape.density(layout, rx, y, rz, natural,
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
if (geology28 && natural > 0 && y > 40 && y < 232)
natural = CavesRelief28.carve(natural, caves.sampleValue(sample, x, y - CavesRelief28.CAVE_OFFSET, z), y);
return natural;
}
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
}
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
}
/** Exact accepted28 deformation and caves, without the fine local ledges. */
public PopulationIslandDensity legacy28() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, false, false);
}
public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
@@ -57,7 +130,8 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
}
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.worldgen;
/** Three broad areas of signed deformation; the intervening rock keeps the original plateau field. */
public final class Relief30 {
private Relief30() {}
public static double influence(long seed, int x, int z, double radius) {
long mixed = seed ^ 0x30BADC0FFEEL;
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
double rotation = (mixed >>> 11) * 0x1.0p-53 * Math.PI * 2;
double strongest = 0;
for (int i = 0; i < 3; i++) {
double angle = rotation + i * Math.PI * 2 / 3;
double cx = Math.cos(angle) * radius * .52, cz = Math.sin(angle) * radius * .52;
double dx = x - cx, dz = z - cz;
double along = (dx * Math.cos(angle) + dz * Math.sin(angle)) / (radius * .47);
double across = (-dx * Math.sin(angle) + dz * Math.cos(angle)) / (radius * .34);
double distance = Math.hypot(along, across);
double t = Math.clamp((1 - distance) / .70, 0, 1);
strongest = Math.max(strongest, t * t * (3 - 2 * t));
}
return strongest;
}
}
@@ -91,6 +91,55 @@ public final class RiftShape {
return (float) Math.max(-1.0, terrain - erosion);
}
/** Alpha28 fractures lean sideways, end within the mass and retain interrupted rock spans. */
public static boolean mayAffect28(Layout layout, int x, int z) {
double radius = Math.hypot((double) x, z);
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
for (Rift rift : layout.rifts()) {
if (near28(rift.main(), x, z)) return true;
for (Segment branch : rift.branches()) if (near28(branch, x, z)) return true;
}
return false;
}
public static float density28(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
if (terrain <= 0 || y < 88 || y > 244 || !mayAffect28(layout, x, z)) return terrain;
double coarse = Math.clamp(warped, -1, 1), fine = Math.clamp(detail, -1, 1), cut = 0;
for (Rift rift : layout.rifts()) {
cut = Math.max(cut, erosion28(rift.main(), rift.id(), x, y, z, coarse, fine));
for (Segment branch : rift.branches())
cut = Math.max(cut, erosion28(branch, rift.id(), x, y, z, coarse, fine));
}
double radius = Math.hypot((double) x, z);
cut *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16)
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20);
return (float) Math.max(-1, terrain - cut);
}
private static boolean near28(Segment segment, int x, int z) {
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 16
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 20;
}
private static double erosion28(Segment segment, long id, int x, int y, int z, double coarse, double fine) {
if (!near28(segment, x, z)) return 0;
double rx = x - segment.x(), rz = z - segment.z();
double along = rx * segment.dx() + rz * segment.dz();
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
double centerY = 150 + unit(id + 41) * 32 + end * .12 + Math.sin(end / 29 + segment.phase()) * 7;
double halfHeight = 22 + unit(id + 42) * 14;
double vertical = smoothstep((halfHeight - Math.abs(y - centerY)) / 10);
if (vertical <= 0) return 0;
double lean = (y - centerY) * (.13 + unit(id + 43) * .07);
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end) - lean - coarse * 1.05;
double width = Math.clamp(segment.halfWidth() + fine * .5 + coarse * .45, 2.2, 5.7);
double distance = Math.hypot(sideways, along - end);
double bridgeAt = (unit(id + 44) - .5) * segment.halfLength();
double bridge = smoothstep((Math.abs(end - bridgeAt) - 3) / 5);
return 2.1 * smoothstep((width + 1.6 - distance) / 2.6) * vertical * bridge;
}
private static boolean near(Segment segment, int x, int z) {
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
@@ -0,0 +1,146 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
import java.util.*;
/** One island-scale route follows actual rock, with local water levels and short open banks. */
public final class River30 {
private static final int STEP = 8;
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
private River30() {}
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
private static int x(long k) { return (int)(k >> 32); }
private static int z(long k) { return (int)k; }
private record Node(long key, double cost) {}
private static final class Rock {
final Sampler sampler; final Map<Long,Integer> tops = new HashMap<>(); int samples;
Rock(Sampler sampler) { this.sampler = sampler; }
boolean solid(int x,int y,int z) { samples++; return y > 1 && y < 384 && sampler.sample(x,y,z) > 0; }
int top(int x,int z) {
return tops.computeIfAbsent(key(x,z), unused -> {
for (int y=358;y>=140;y--) if (solid(x,y,z)) {
for(int d=1;d<=6;d++) if(!solid(x,y-d,z)) return -1;
return y;
}
return -1;
});
}
}
public static Plan create(long seed, int radius, Sampler sampler) {
var rock = new Rock(sampler); var nodes = new TreeMap<Long,Integer>();
int limit = radius / STEP * STEP;
for(int x=-limit;x<=limit;x+=STEP) for(int z=-limit;z<=limit;z+=STEP) {
if(Math.hypot(x,z)>radius*.95 || Math.hypot(x,z)<52) continue;
int y=rock.top(x,z); if(y>=140) nodes.put(key(x,z),y);
}
// Ignore isolated peripheral rock when selecting opposite banks: route in the largest connected mass.
var adjacency=new HashMap<Long,List<Long>>();
for(long k:nodes.keySet()) {
var neighbours=new ArrayList<Long>();
for(var d:DIR) {
long next=key(x(k)+d[0]*STEP,z(k)+d[1]*STEP);
if(!nodes.containsKey(next)||Math.abs(nodes.get(next)-nodes.get(k))>48)continue;
int gaps=0;for(int n=1;n<STEP;n++)if(rock.top(x(k)+d[0]*n,z(k)+d[1]*n)<140)gaps++;
if(gaps<=2)neighbours.add(next);
}
adjacency.put(k,neighbours);
}
var remaining=new TreeSet<>(nodes.keySet());Set<Long> largest=Set.of();
while(!remaining.isEmpty()) {
var component=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(remaining.first());
while(!queue.isEmpty()){long k=queue.remove();if(!component.add(k))continue;queue.addAll(adjacency.get(k));}
remaining.removeAll(component);if(component.size()>largest.size())largest=component;
}
nodes.keySet().retainAll(largest);
List<Long> best = List.of(); double longest = 0;
// Four deterministic orientations avoid choosing an axis whose shore is entirely broken.
for(int axis=0;axis<4;axis++) {
double angle=(seed & 1023)*Math.PI/512 + axis*Math.PI/4, dx=Math.cos(angle), dz=Math.sin(angle);
var ordered=nodes.keySet().stream().sorted(Comparator.<Long>comparingDouble(k->x(k)*dx+z(k)*dz).thenComparingLong(k->k)).toList();
if(ordered.isEmpty()) break;
long start=ordered.getFirst(); var costs=new HashMap<Long,Double>(); var parent=new HashMap<Long,Long>();
var queue=new PriorityQueue<Node>(Comparator.comparingDouble(Node::cost).thenComparingLong(Node::key));
costs.put(start,0.0);parent.put(start,start);queue.add(new Node(start,0));
while(!queue.isEmpty()) {
var at=queue.remove(); if(at.cost()!=costs.get(at.key())) continue;
for(long next:adjacency.get(at.key())) {
if(!nodes.containsKey(next)) continue;
int difference=Math.abs(nodes.get(next)-nodes.get(at.key()));
double cost=at.cost()+STEP+difference*difference*.18+Math.max(0,nodes.get(next)-215)*.13;
if(cost>=costs.getOrDefault(next,Double.POSITIVE_INFINITY)) continue;
costs.put(next,cost);parent.put(next,at.key());queue.add(new Node(next,cost));
}
}
for(int i=ordered.size()-1;i>=0;i--) {
long end=ordered.get(i); if(!parent.containsKey(end)) continue;
double span=Math.hypot(x(end)-x(start),z(end)-z(start));
if(span>longest) { var path=new ArrayList<Long>();for(long k=end;;k=parent.get(k)){path.add(k);if(k==start)break;}Collections.reverse(path);best=path;longest=span; }
break;
}
}
var cells=new LinkedHashMap<Long,Cell>();var path=new ArrayList<Point>(); long id=0x300000000L;
if(longest>=radius*.7) for(int i=1;i<best.size();i++) {
long a=best.get(i-1),b=best.get(i);int dx=Integer.signum(x(b)-x(a)),dz=Integer.signum(z(b)-z(a));
for(int n=0;n<=STEP;n++) {
int cx=x(a)+dx*n,cz=z(a)+dz*n;path.add(new Point(cx,cz));
int center=rock.top(cx,cz);
for(int side=-2;side<=2;side++) {
int px=cx+dz*side,pz=cz-dx*side,top=rock.top(px,pz);
if(top<140 || Math.abs(top-center)>5) continue;
// The bed stays in existing rock. Higher neighbouring faces become natural banks.
int level=Math.min(top-1,center-1),bed=level-2;boolean support=true;
for(int y=bed-3;y<=bed;y++) if(!rock.solid(px,y,pz)) {support=false;break;}
if(support) cells.put(key(px,pz),new Cell(px,pz,level,bed,top,side==0?ShoreMaterial.GRAVEL:ShoreMaterial.CLAY,id,2));
}
}
}
System.getLogger(River30.class.getName()).log(System.Logger.Level.INFO,"River30 connectedNodes="+nodes.size()+" span="+longest+" columns="+rock.tops.size());
var features=new ArrayList<Feature>();
if(!cells.isEmpty()) features.add(feature(id,Kind.RIVER,cells.values(),path));
var plan=PopulationHydrology.additional30(cells,features,List.of(),rock.tops.size(),rock.samples);
return openBanks(seed,plan,sampler,6);
}
/** Cut only a short bank. Nothing is built beneath a fall and its landing is unrestricted. */
public static Plan openBanks(long seed,Plan water,Sampler sampler,int maximum) {
var cuts=new LinkedHashMap<Long,Cell>();var features=new ArrayList<Feature>();var spills=new ArrayList<Spill>();
var mouths=new ArrayList<Point>();
var candidates=water.cells().stream().filter(Cell::hasWater).sorted(Comparator
.<Cell>comparingLong(c->mix(seed^key(c.x(),c.z()))).thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
for(var wet:candidates) {
if(mouths.size()>=maximum) break;
if(mouths.stream().anyMatch(p->Math.hypot(p.x()-wet.x(),p.z()-wet.z())<48)) continue;
for(var d:DIR) {
if(water.cellAt(wet.x()+d[0],wet.z()+d[1])!=null) continue;
int reach=0;
for(int n=1;n<=5;n++) {
int px=wet.x()+d[0]*n,pz=wet.z()+d[1]*n;
if(sampler.sample(px,wet.waterY(),pz)<=0 && sampler.sample(px,wet.waterY()-3,pz)<=0
&& sampler.sample(px,wet.waterY()-6,pz)<=0) {reach=n;break;}
}
if(reach==0) continue;
long id=0x310000000L+(mix(seed)&0xfffffffL)*16+mouths.size();var local=new LinkedHashMap<Long,Cell>();
boolean conflict=false;
for(int n=1;n<=reach;n++) for(int side=-1;side<=1;side++) {
int px=wet.x()+d[0]*n+d[1]*side,pz=wet.z()+d[1]*n-d[0]*side;
var old=water.cellAt(px,pz);
if(old!=null && old.hasWater()) {conflict=true;continue;}
local.put(key(px,pz),new Cell(px,pz,-1,wet.waterY()-1,wet.waterY()+2,ShoreMaterial.STONE,id,0));
}
if(conflict) continue;
cuts.putAll(local);features.add(feature(id,Kind.OUTLET,local.values(),List.of()));mouths.add(new Point(wet.x(),wet.z()));
var source=new Position(wet.x(),wet.waterY(),wet.z());
var outlet=new Position(wet.x()+d[0]*reach,wet.waterY(),wet.z()+d[1]*reach);
var bounds=new FlowBounds(Math.min(source.x(),outlet.x())-3,0,Math.min(source.z(),outlet.z())-3,
Math.max(source.x(),outlet.x())+3,wet.waterY()+3,Math.max(source.z(),outlet.z())+3);
spills.add(new Spill(id,wet.featureId(),-1,source,outlet,List.of(source,outlet),bounds));break;
}
}
return water.overlay30(PopulationHydrology.additional30(cuts,features,spills,0,0));
}
private static Feature feature(long id,Kind kind,Collection<Cell> cells,List<Point> path) {
int x0=Integer.MAX_VALUE,z0=x0,x1=Integer.MIN_VALUE,z1=x1,water=-1,count=0;
for(var c:cells){x0=Math.min(x0,c.x());z0=Math.min(z0,c.z());x1=Math.max(x1,c.x());z1=Math.max(z1,c.z());if(c.hasWater()){water=c.waterY();count++;}}
return new Feature(id,kind,water,path,count,new Bounds(x0,z0,x1,z1));
}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
}
@@ -109,6 +109,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
}
public boolean aerialDryWitness30(int x, int groundY, int z) {
var current = originContext;
if (current == null) throw new IllegalStateException("Aerial sampling before world attachment");
for (var cell : PopulationHydrologyRuntime.cellsAt(current.generator(), current.random(), x, z))
if (groundY >= cell.bedY() && groundY <= Math.max(cell.carveTop(), cell.waterY()) + 1) return false;
return true;
}
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
private boolean mineralPrepared23;
@@ -183,7 +190,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int 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;
&& minY <= site.maxY() + 1 && maxY >= site.minY() - 1) return true;
if (site.waterfall()) {
var flow = site.waterfallClaimBounds();
if (minX <= flow.maxX() && maxX >= flow.minX() && minZ <= flow.maxZ() && maxZ >= flow.minZ()
&& minY <= flow.maxY() && maxY >= flow.minY()) 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)
@@ -210,6 +222,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
case "sanctuary:population_rocky_heath" -> "meadow";
case "sanctuary:population_dark_grove" -> "dark_forest";
case "sanctuary:population_bamboo_grove" -> "bamboo_jungle";
case "sanctuary:swamp27_caves" -> "swamp";
case "sanctuary:population_lush_caves" -> "lush_caves";
case "sanctuary:population_dripstone_caves" -> "dripstone_caves";
case "sanctuary:population_sulfur_depths" -> "sulfur_caves";
default -> null;
};
return equivalent == null ? biome : registries.lookupOrThrow(Registries.BIOME).getOrThrow(
@@ -335,11 +351,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = generation >= 22
transitPlan21 = generation == 24
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation >= 22
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
if ((generation == 22 || generation == 23) && 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()));
@@ -388,7 +406,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
pos -> protectsDecoration(pos) || protectsCore22(pos));
long started = System.nanoTime();
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan30(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
new fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.Terrain() {
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public int surfaceY(int x, int z) { return sampled.ground(x, z).top(); }
@@ -14,7 +14,7 @@ public final class Swamp27Volumes {
// Broad, connected sectors change with height; finer noise softens their borders.
double broad = noise(seed, x / 94.0, y / 48.0, z / 94.0);
double detail = noise(seed ^ 0x7757A27L, x / 37.0, y / 31.0, z / 37.0);
return broad + detail * .22 > .04 + (1 - verticalEdge) * .7;
return broad + detail * .22 > .25 + (1 - verticalEdge) * .7;
}
private static double noise(long seed, double x, double y, double z) {
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.worldgen;
/** Small, locally phased ledges on the accepted three-dimensional deformation. */
public final class Terraces29 {
public static final double MAX_OFFSET = 2.5;
public static final int START_Y = 214;
private Terraces29() {}
public static double weight(int y, double deformationWeight) {
return smooth((y - START_Y) / 30.0) * deformationWeight;
}
/** Period and phase both follow 3D noise; there is no shared horizontal layer grid. */
public static double sourceY(double originalY, float coarse, float detail, double weight) {
if (weight <= 0) return originalY;
double period = 7.5 + 1.5 * Math.tanh(coarse * .9);
double phase = 7 * Math.tanh(coarse * .7) + 2 * Math.tanh(detail * .65);
double band = (originalY + phase) / period;
double fraction = band - Math.floor(band);
double shaped = smooth((fraction - .3) / .4);
double offset = Math.clamp((shaped - fraction) * period * .9, -MAX_OFFSET, MAX_OFFSET);
return originalY + offset * Math.clamp(weight, 0, 1);
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -15,11 +15,15 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192;
public static final int ARRIVAL_RADIUS = 96;
public static final int MIN_ISLET_GAP = 16;
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; }
/** The raw noise surface may subsequently be cut into a pond or an open river bank. */
default boolean dryWitness(int x, int groundY, int z) { return true; }
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
@@ -28,6 +32,9 @@ public final class AerialPlanner24 {
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 dryWitness(int x, int groundY, int z) {
return generator.aerialDryWitness30(x, groundY, z);
}
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
@@ -56,26 +63,68 @@ public final class AerialPlanner24 {
// Keep the four remaining bearings and seeded shapes; the former first sector is simply free.
double angle = initialAngle + (ordinal + 1) * (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 = merchant ? 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);
Shore shore;
int cx, cz, bottom;
if (merchant) {
shore = shore(probe, angle, radius);
if (shore == null) continue;
// Start from the real edge, not the nominal island radius. The projected half-box
// puts the complete hull 1632 blocks out, even when the ship is rotated.
double dx = Math.cos(angle), dz = Math.sin(angle);
double offset = Math.abs(dx) * worldWidth / 2.0 + Math.abs(dz) * worldDepth / 2.0
+ 16 + (attempt % 3) * 8;
cx = shore.x + (int) Math.round(dx * offset);
cz = shore.z + (int) Math.round(dz * offset);
// Native boarding deck is five blocks above the envelope's bottom.
bottom = shore.y - 5 + (int) Math.floorMod(draw, 5);
} else {
// Both mineral islands hover over Sanctuary, away from its natural arrival.
double distance = radius * (.48 + .15 * unit(mix(draw ^ 0x28A1L)));
cx = (int) Math.round(Math.cos(angle) * distance);
cz = (int) Math.round(Math.sin(angle) * distance);
int top = probe.surface(cx, cz);
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue;
shore = dryViewpoint(probe, cx, cz);
if (shore == null) continue;
int highest = top;
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
bottom = highest + MIN_ISLET_GAP + 1;
}
if (bottom < 32 || bottom + height > 350) continue;
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 == 2);
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 + bodyHeight - 4, cz)) continue;
if (!outsideArrival(site) || site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
if (merchant) {
double gap = shoreDistance(site);
if (gap < 16 || gap > 48 || !clearShip(probe, site)) continue;
} else {
// Certify every physical rock column, not only the coarse samples used to
// choose a height. This may raise the whole unchanged island, never the ground.
int highest = 0, supported = 0, count = 0;
for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) {
if (!isletColumn(site, x, z)) continue;
int wx = site.worldX(x, z), wz = site.worldZ(x, z);
highest = Math.max(highest, probe.highest(wx, wz));
if (probe.surface(wx, wz) >= 120) supported++;
count++;
}
if (supported * 4 < count * 3) continue;
if (site.waterfall()) {
var outlet = site.waterfallClaimBounds();
for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++)
highest = Math.max(highest, probe.highest(x, z));
}
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
if (bottom + height > 350) continue;
site = new Site(site.id(), site.kind(), site.minX(), bottom, site.minZ(), width, height, depth,
facing, siteSeed, shore.x, shore.y, shore.z, site.waterfall());
if (!clearReservations(probe, site)) continue;
}
if (!visible(probe, shore, cx, bottom + 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");
@@ -89,6 +138,39 @@ public final class AerialPlanner24 {
return result;
}
/** Horizontal distance from the saved land witness to the nearest point of the hull envelope. */
public static double shoreDistance(Site site) {
return Math.hypot(site.shoreX() - Math.clamp(site.shoreX(), site.minX(), site.maxX()),
site.shoreZ() - Math.clamp(site.shoreZ(), site.minZ(), site.maxZ()));
}
private static boolean outsideArrival(Site site) {
if (Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) < ARRIVAL_RADIUS) return false;
if (site.waterfall()) {
var box = site.waterfallClaimBounds();
if (Math.hypot(Math.clamp(0, box.minX(), box.maxX()), Math.clamp(0, box.minZ(), box.maxZ())) < ARRIVAL_RADIUS) return false;
}
return true;
}
private static boolean isletColumn(Site site, int x, int z) {
return AerialGeometry24.radial(site, x, z) <= 1
|| site.waterfall() && Math.abs(x - site.width() / 2) <= 2 && z <= 6;
}
private static boolean clearShip(Probe probe, Site site) {
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
if (probe.highest(x, z) >= site.minY() - 3 || probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
return true;
}
private static boolean clearReservations(Probe probe, Site site) {
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
if (probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
if (site.waterfall()) {
var box = site.waterfallClaimBounds();
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
if (probe.terrain.reserved(x, z, box.minY(), box.maxY())) return false;
}
return true;
}
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.
@@ -120,6 +202,19 @@ public final class AerialPlanner24 {
return true;
}
private record Shore(int x, int y, int z) {}
private static Shore dryViewpoint(Probe probe, int x, int z) {
// Keep the site above the same terrain. Move only its saved walking viewpoint
// to a nearby dry bank when the centre is subsequently carved by hydrology.
for (int r = 0; r <= 12; r += 2) for (int dx = -r; dx <= r; dx += 2)
for (int dz = -r; dz <= r; dz += 2) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
int top = probe.surface(x + dx, z + dz);
if (top >= 120 && probe.terrain.dryWitness(x + dx, top, z + dz)
&& !probe.terrain.reserved(x + dx, z + dz, top, top + 3))
return new Shore(x + dx, top + 1, z + dz);
}
return null;
}
private static final class Probe {
final Terrain terrain; final int radius;
final Map<Long, NoiseColumn> columns = new HashMap<>();
@@ -138,6 +233,11 @@ public final class AerialPlanner24 {
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
return 0;
}
int highest(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).isAir()) return y;
return 0;
}
boolean solid(int x, int y, int z) {
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
}
@@ -95,13 +95,9 @@ public final class AerialStore24 {
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");
}
// Aerial bodies may share X/Z with the initial island. Their vertical clearance is
// certified at creation; the immutable manifest preserves it on subsequent loads.
// Site/Plan constructors still validate finite height, dimensions, count and disjoint claims.
}
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
var root = new JsonObject();
@@ -22,7 +22,7 @@ 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 four immutable initial sites. Their void chunks need no terrain island owner. */
/** Native starts for four immutable initial sites, in the void or above initial terrain. */
public final class AerialStructures24 {
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
@@ -67,7 +67,10 @@ public final class AerialStructures24 {
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);
// Only this explicit saved departure bypasses biome selection. Its complete
// geometry and clearance were certified by the aerial planner; the biome below
// may be forest, marsh or void. findGenerationPoint still requires this exact plan.
chunk, ignoredBiome -> sites.stream().anyMatch(site -> key(site).equals(key)));
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
}
}
@@ -5,7 +5,7 @@ import java.util.Map;
/** Finite terrain search: at most48 horizontal candidates, four orientations, three depths. */
public final class MineralSanctuaryPlanner23 {
public static final int MAX_COLUMNS = 10_000, MAX_SAMPLES = 1_200_000;
public static final int MAX_COLUMNS = 24_000, MAX_SAMPLES = 2_400_000;
private MineralSanctuaryPlanner23() {}
public interface Terrain {
boolean owns(int x, int z);
@@ -14,12 +14,19 @@ public final class MineralSanctuaryPlanner23 {
boolean protectedAt(int x, int y, int z);
}
public static MineralSanctuaryPlan23 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, false);
}
public static MineralSanctuaryPlan23 plan30(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, true);
}
private static MineralSanctuaryPlan23 search(long seed, int cx, int cz, int diameter, Terrain terrain, boolean exposedFallback) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported mineral island size");
var p = new Probe(terrain);
try {
for (int attempt = 0; attempt < 48; attempt++) {
for (int attempt = 0; attempt < (exposedFallback ? 96 : 48); attempt++) {
double angle = unit(mix(seed ^ 0x23CA7EL)) * Math.PI * 2 + attempt * 2.399963229728653;
double radius = diameter * (.17 + .045 * (attempt % 4));
p.exposed = attempt >= 48;
double radius = diameter * (p.exposed ? .27 + .04 * (attempt % 5) : .17 + .045 * (attempt % 4));
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int surface = p.surface(x, z);
if (surface < 80) { p.reject("candidate-surface"); continue; }
@@ -39,7 +46,7 @@ public final class MineralSanctuaryPlanner23 {
var result = new MineralSanctuaryPlan23(c, p.samples, p.columns.size(), p.rejections);
System.getLogger(MineralSanctuaryPlanner23.class.getName()).log(System.Logger.Level.INFO,
"Mineral23 accepted=" + (c != null) + " columns=" + p.columns.size() + " samples=" + p.samples
+ " chamber=" + c + " rejected=" + p.rejections);
+ " exposed=" + p.exposed + " chamber=" + c + " rejected=" + p.rejections);
return result;
}
/** The fissure ends in existing traversable air (a cave or the exposed island side), not sealed rock. */
@@ -81,9 +88,9 @@ public final class MineralSanctuaryPlanner23 {
tested++;
}
}
if (columns == 0 || covered * 5 < columns * 3 || coveredQuadrants != 15)
if (columns == 0 || covered * 5 < columns * (p.exposed ? 1 : 3) || (p.exposed ? Integer.bitCount(coveredQuadrants) < 2 : coveredQuadrants != 15))
return p.reject("coarse-cover-distribution");
return tested > 0 && rock * 5 >= tested * 3 || p.reject("coarse-rock");
return tested > 0 && rock * 5 >= tested * (p.exposed ? 1 : 3) || p.reject("coarse-rock");
}
/** Reject unsupported candidates using a small shared column lattice before reading the full floor. */
private static boolean coarseBearings(MineralSanctuaryPlan23.Chamber c, Probe p) {
@@ -98,12 +105,12 @@ public final class MineralSanctuaryPlanner23 {
else gaps.add(point);
}
// Sampling is deliberately permissive about the proportion; dense certification still requires60%.
if (anchors.size() < gaps.size()) return p.reject("coarse-anchor-majority");
if (anchors.size() * (p.exposed ? 2 : 1) < gaps.size()) return p.reject("coarse-anchor-majority");
for (var gap : gaps) {
boolean supported = false;
for (var anchor : anchors) {
int dx = gap[0] - anchor[0], dz = gap[1] - anchor[1];
if (dx * dx + dz * dz <= 100) { supported = true; break; }
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100)) { supported = true; break; }
}
if (!supported) return p.reject("coarse-anchor-distance");
}
@@ -137,14 +144,14 @@ public final class MineralSanctuaryPlanner23 {
anchored[x][z] = true; direct++;
}
}
if (columns == 0 || direct * 5 < columns * 3) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * 3) return p.reject("full-cover-distribution");
if (columns == 0 || direct * 5 < columns * (p.exposed ? 2 : 3)) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * (p.exposed ? 1 : 3)) return p.reject("full-cover-distribution");
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
if (!footprint[x][z] || anchored[x][z]) continue;
boolean nearby = false;
for (int dz = -10; dz <= 10 && !nearby; dz++) for (int dx = -10; dx <= 10; dx++) {
for (int dz = p.exposed ? -18 : -10; dz <= (p.exposed ? 18 : 10) && !nearby; dz++) for (int dx = p.exposed ? -18 : -10; dx <= (p.exposed ? 18 : 10); dx++) {
int ax = x + dx, az = z + dz;
if (dx * dx + dz * dz <= 100 && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100) && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
&& anchored[ax][az]) { nearby = true; break; }
}
if (!nearby) return p.reject("floor-anchor-distance");
@@ -158,7 +165,7 @@ public final class MineralSanctuaryPlanner23 {
}
private static final class Probe {
final Terrain terrain; final Map<Long, Integer> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples;
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples; boolean exposed;
Probe(Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
int surface(int x, int z) {
if (!terrain.owns(x, z)) return -1;
@@ -50,6 +50,9 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
}
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
// Intact neighboring rock can carry the continuous masonry walls and roof.
if (x >= minX - 1 && x <= maxX() + 1 && z >= minZ - 1 && z <= maxZ() + 1
&& y >= minY && y <= maxY() + 2) return true;
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
// Two untouched layers carry the slab and the certified short piers.
@@ -78,8 +81,8 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() + 1 && x1 >= m.minX() - 1 && z0 <= m.maxZ() + 1 && z1 >= m.minZ() - 1
&& y0 <= m.maxY() + 2 && y1 >= m.boundsMinY() - 2) return true;
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
@@ -0,0 +1,194 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
import fr.koka.sanctuary.worldgen.transit.TransitPlanner21.Terrain;
import java.util.*;
/** The established road and stair shafts, fitted from their entrances before their depth.
* Only the indispensable traversal uses these civil-engineering bearings. No terrain is added. */
public final class TransitPlanner29 {
public static final int MAX_COLUMNS = 12_000;
private TransitPlanner29() {}
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit diameter");
var p = new Probe(terrain); int layouts = 0; String reason = "no natural entrance pair";
var entries = new ArrayList<Alignment>();
int mainRadius = Math.max(68, (int) Math.round(diameter * .19 / 17) * 17);
var radii = new LinkedHashSet<Integer>();
for (int radius : new int[]{mainRadius, mainRadius - 17, mainRadius - 34, mainRadius + 17})
if (radius * 2 >= diameter / 4) radii.add(radius);
try {
// Cheap, cached entrance scans precede any expensive underground volume scan.
search: for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
for (int radius : radii) for (boolean alongX : new boolean[]{true, false})
for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
-3 * diameter / 16, 3 * diameter / 16}) {
if (++layouts > 320 || p.columns.size() >= 3_000) break search;
int ax = cx + (alongX ? advance - radius : shift), az = cz + (alongX ? shift : advance - radius);
int bx = cx + (alongX ? advance + radius : shift), bz = cz + (alongX ? shift : advance + radius);
int a = entrance(ax, az, p), b = entrance(bx, bz, p);
if (a < 0 || b < 0 || Math.abs(a - b) > 95) continue;
entries.add(new Alignment(ax, az, bx, bz, a, b, alongX));
if (entries.size() >= 48) break search;
}
// Preserve every complete route already accepted by the primary search. Only after
// its failure may an incompatible optional footing be omitted, using other bearings.
for (boolean selectiveFootings : new boolean[]{false, true}) {
p.selectiveFootings = selectiveFootings;
for (var a : entries) {
var floors = new LinkedHashSet<Integer>(); int low = Math.min(a.topA, a.topB), high = Math.max(a.topA, a.topB);
for (int floor : new int[]{low - 26, low - 42, low - 58, low - 74, 200, 184, 160})
if (floor >= 40 && floor <= low - 18 && floor >= high - 140) floors.add(floor);
for (int floor : floors) {
var modules = route(seed, a, floor, p, false);
if (modules == null) { reason = p.reason; continue; }
var complete = new ArrayList<Module>();
for (var module : modules) {
var certified = certify(module, p, true);
if (certified == null) { reason = p.reason; break; }
complete.add(certified);
}
if (complete.size() != modules.size()) continue;
addHall(complete, floor, a.alongX, seed, p);
return new TransitPlan21(complete, p.samples, "accepted29 axis=" + (a.alongX ? "east-west" : "north-south")
+ " floor=" + floor + " span=" + Math.max(Math.abs(a.bx - a.ax), Math.abs(a.bz - a.az))
+ " entries=" + entries.size() + " layouts=" + layouts + " columns=" + p.columns.size()
+ (selectiveFootings ? " selective-footings" : ""));
}
}
}
} catch (Budget exhausted) { reason = "bounded29 search exhausted: " + p.reason; }
return new TransitPlan21(List.of(), p.samples, reason + "; entries=" + entries.size() + "; layouts=" + layouts + "; columns=" + p.columns.size());
}
/** A three-wide doorstep is sufficient; the surrounding hillside need not be a flat platform. */
private static int entrance(int cx, int cz, Probe p) {
long key = key(cx, cz); Integer known = p.entrances.get(key); if (known != null) return known;
int low = 384, high = -1;
for (int x = cx - 1; x <= cx + 1; x++) {
int y = p.surface(x, cz - 9); low = Math.min(low, y); high = Math.max(high, y);
}
boolean valid = low >= 65 && high <= 360 && high - low <= 1;
if (valid) for (int x = cx - 1; x <= cx + 1; x++) {
int y = p.surface(x, cz - 9);
if (p.protectedAt(x, y, cz - 9) || p.protectedAt(x, high + 1, cz - 9)
|| p.protectedAt(x, high + 2, cz - 9)) valid = false;
}
p.entrances.put(key, valid ? high : -1); return valid ? high : -1;
}
private record Alignment(int ax, int az, int bx, int bz, int topA, int topB, boolean alongX) {}
private static void addHall(List<Module> route, int floor, boolean alongX, long seed, Probe p) {
int index = 1 + (route.size() - 2) / 2; var junction = route.get(index);
for (int side : new int[]{-1, 1}) {
try {
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 9 : 17, 10,
alongX ? 17 : 9, alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x29B1L ^ side, List.of()), p, true);
if (passage == null || !p.anchored.getOrDefault(passage, false)) continue;
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x29A1L ^ side, List.of()), p, true);
if (hall == null) continue;
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
var joined = certify(new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
junction.seed(), junction.supports()), p, true);
if (joined == null) continue;
route.set(index, joined);
route.add(passage); route.add(hall); return;
} catch (Budget exhausted) { return; }
}
}
private static List<Module> route(long seed, Alignment a, int floor, Probe p, boolean full) {
var result = new ArrayList<Module>();
var first = certify(new Module(Kind.ACCESS, a.ax - 8, floor - 1, a.az - 8, 17, a.topA - floor + 6,
17, 0, a.alongX ? 2 : 4, seed ^ 0x2901L, List.of()), p, full);
if (first == null) return null;
var last = certify(new Module(Kind.ACCESS, a.bx - 8, floor - 1, a.bz - 8, 17, a.topB - floor + 6,
17, 0, a.alongX ? 8 : 1, seed ^ 0x2902L, List.of()), p, full);
if (last == null) return null;
result.add(first); int length = Math.max(Math.abs(a.bx - a.ax), Math.abs(a.bz - a.az)), weak = 0;
for (int d = 17; d < length; d += 17) {
int x = a.ax + (a.alongX ? d : 0), z = a.az + (a.alongX ? 0 : d);
var m = certify(new Module(Kind.GALLERY, x - (a.alongX ? 8 : 4), floor - 1, z - (a.alongX ? 4 : 8),
a.alongX ? 17 : 9, 10, a.alongX ? 9 : 17, a.alongX ? 1 : 0, a.alongX ? 10 : 5,
seed ^ (d * 0x9E3779B97F4A7C15L), List.of()), p, full);
if (m == null) return null;
weak = p.anchored.getOrDefault(m, false) ? 0 : weak + 1;
if (weak > 2) { p.reason = "unsupported span exceeds34 blocks"; return null; }
result.add(m);
}
result.add(last); return result;
}
private static Module certify(Module m, Probe p, boolean full) {
var supports = new ArrayList<Support>(); int quadrants = 0;
for (int x : new int[]{m.minX() + 1, m.centerX(), m.maxX() - 1})
for (int z : new int[]{m.minZ() + 1, m.centerZ(), m.maxZ() - 1}) {
if (!p.owns(x, z)) { p.reason = "outside island"; return null; }
int anchor = m.minY() - 1;
while (anchor >= m.minY() - 17 && !(p.solid(x, anchor, z) && p.solid(x, anchor - 1, z))) anchor--;
if (anchor >= m.minY() - 17) {
boolean compatible = true;
for (int y = anchor - 1; y < m.minY(); y++) if (p.protectedAt(x, y, z)) {
if (!p.selectiveFootings) { p.reason = "protected footing"; return null; }
compatible = false; break;
}
// Do not place this pier, and do not count its quadrant as a bearing.
// The unchanged wall/roof checks and maximum free span still apply.
if (!compatible) continue;
if (anchor < m.minY() - 1) supports.add(new Support(x, anchor + 1, z, m.minY() - 1));
quadrants |= 1 << ((x < m.centerX() ? 0 : 1) + (z < m.centerZ() ? 0 : 2));
}
}
// Existing continuous masonry can also bear on untouched rock beside its walls or roof.
// These contact cells lie in the protected one-block fringe, never inside excavated air.
int wallMask = 0, roofMask = 0;
for (int y : new int[]{m.minY() + 3, m.minY() + 6, m.kind() == Kind.ACCESS ? Math.min(m.maxY() - 5, m.minY() + 18) : m.maxY()}) {
if ((m.openings() & 8) == 0 && p.solid(m.minX() - 1, y, m.centerZ()) && p.solid(m.minX() - 1, y + 1, m.centerZ())) wallMask |= 1;
if ((m.openings() & 2) == 0 && p.solid(m.maxX() + 1, y, m.centerZ()) && p.solid(m.maxX() + 1, y + 1, m.centerZ())) wallMask |= 2;
if ((m.openings() & 1) == 0 && p.solid(m.centerX(), y, m.minZ() - 1) && p.solid(m.centerX(), y + 1, m.minZ() - 1)) wallMask |= 4;
if ((m.openings() & 4) == 0 && p.solid(m.centerX(), y, m.maxZ() + 1) && p.solid(m.centerX(), y + 1, m.maxZ() + 1)) wallMask |= 8;
}
if (m.kind() != Kind.ACCESS) for (int x : new int[]{m.minX() + 1, m.maxX() - 1})
for (int z : new int[]{m.minZ() + 1, m.maxZ() - 1}) if (p.solid(x, m.maxY() + 1, z) && p.solid(x, m.maxY() + 2, z))
roofMask |= 1 << ((x < m.centerX() ? 0 : 1) + (z < m.centerZ() ? 0 : 2));
boolean anchored = quadrants == 15 || roofMask == 15 || (wallMask & 3) == 3 || (wallMask & 12) == 12;
if (!anchored && m.kind() != Kind.GALLERY) { p.reason = "no civil-engineering bearing for " + m.kind(); return null; }
int step = full ? 1 : 4;
for (int x = m.minX(); x <= m.maxX(); x += step) for (int z = m.minZ(); z <= m.maxZ(); z += step) {
if (!p.owns(x, z)) { p.reason = "outside island"; return null; }
for (int y = m.minY(); y <= m.maxY(); y += step) if (p.protectedAt(x, y, z)) {
p.reason = "protected " + m.kind() + " at " + x + "," + y + "," + z; return null;
}
}
var result = new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
m.facing(), m.openings(), m.seed(), supports);
p.anchored.put(result, anchored); return result;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static final class Probe {
final Terrain terrain; final Set<Long> columns = new HashSet<>();
final Map<Long, Integer> entrances = new HashMap<>(); final Map<Module, Boolean> anchored = new HashMap<>();
int samples; boolean selectiveFootings; String reason = "budget";
Probe(Terrain terrain) { this.terrain = terrain; }
void visit(int x, int z) {
if (samples >= TransitPlan21.MAX_SAMPLES) throw new Budget(); samples++;
long key = key(x, z);
if (!columns.contains(key) && columns.size() >= MAX_COLUMNS) throw new Budget(); columns.add(key);
}
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
}
private static final class Budget extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -26,7 +26,8 @@ public final class TransitStructure21 extends Structure {
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
&& module.kind() == TransitPlan21.Kind.JUNCTION
&& (generator.rootGeneration() == 24 || Integer.bitCount(module.openings()) == 2)
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
@@ -58,7 +58,9 @@ public final class TransitStructures21 {
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, holder.value().biomes()::contains);
// The route is already certified as one connected whole. A 3D biome boundary
// must not silently remove a local segment or one of its two surface accesses.
chunk, generator.rootGeneration() == 24 ? biome -> true : holder.value().biomes()::contains);
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
}
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
@@ -0,0 +1,58 @@
{
"type": "minecraft:min",
"left": {
"type": "minecraft:max",
"left": {
"type": "minecraft:min",
"left": {
"type": "minecraft:min",
"left": {
"type": "minecraft:add",
"left": {
"type": "minecraft:mul",
"left": {
"type": "minecraft:square",
"input": {
"type": "minecraft:noise",
"noise": "minecraft:cave_layer",
"xz_scale": 1.0,
"y_scale": 8.0
}
},
"right": 4.0
},
"right": {
"type": "minecraft:clamp",
"input": {
"type": "minecraft:add",
"left": {
"type": "minecraft:noise",
"noise": "minecraft:cave_cheese",
"xz_scale": 1.0,
"y_scale": 0.6666666666666666
},
"right": 0.27
},
"max": 1.0,
"min": -1.0
}
},
"right": "minecraft:overworld/caves/entrances"
},
"right": {
"type": "minecraft:add",
"left": "minecraft:overworld/caves/spaghetti_2d",
"right": "minecraft:overworld/caves/spaghetti_roughness_function"
}
},
"right": {
"type": "minecraft:range_choice",
"input": "minecraft:overworld/caves/pillars",
"max_exclusive": 0.03,
"min_inclusive": -1000000.0,
"when_in_range": -1000000.0,
"when_out_of_range": "minecraft:overworld/caves/pillars"
}
},
"right": "minecraft:overworld/caves/noodle"
}
@@ -49,5 +49,21 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
}
@@ -49,5 +49,21 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
}
@@ -49,5 +49,21 @@
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
},
"geology_28": true,
"caves_28": "sanctuary:caves28_native",
"ridges_28": {
"type": "minecraft:noise",
"noise": "minecraft:ridge",
"xz_scale": 1.0,
"y_scale": 1.0
},
"erosion_28": {
"type": "minecraft:noise",
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
},
"terraces_29": false,
"relief_30": true
}
@@ -0,0 +1,83 @@
package fr.koka.sanctuary.expansion;
import java.util.List;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftStructure;
/** Thin native branches must not reserve every empty room between them. Native support reach stays bounded. */
public final class MineshaftAdmission29Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
// Bootstrap does not load datapack tags. This synthetic world contains only STONE/AIR;
// bind its real26.3 heightmap tag so the production native predicate can see its stone ceiling.
var blocks = net.minecraft.core.registries.BuiltInRegistries.BLOCK;
blocks.freeze();
blocks.prepareTagReload(new net.minecraft.tags.TagLoader.LoadResult<>(net.minecraft.core.registries.Registries.BLOCK,
java.util.Map.of(net.minecraft.tags.BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP,
List.of(blocks.wrapAsHolder(Blocks.STONE))))).apply();
require(net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR_WG.isOpaque().test(Blocks.STONE.defaultBlockState()),
"The synthetic stone ceiling is bound to the native heightmap tag");
var branches = List.of(MineshaftAdmission29.body(new BoundingBox(-30,150,-30,-28,152,30)),
MineshaftAdmission29.body(new BoundingBox(28,150,-30,30,152,30)),
MineshaftAdmission29.body(new BoundingBox(-30,150,28,30,152,30)));
var centralSecret = new BoundingBox(-5,145,-5,5,160,5);
require(new BoundingBox(-30,149,-30,30,154,30).intersects(centralSecret), "Old global envelope falsely intersects the central secret");
require(!MineshaftAdmission29.intersects(branches, centralSecret), "Unoccupied room between native branches is not part of the mine");
require(MineshaftAdmission29.intersects(branches, new BoundingBox(-31,151,0,-29,152,1)), "An actual branch remains protected against an intersecting secret");
var layered = column(-1,384);
for (int y = 180; y <= 220; y++) layered.setBlock(y, Blocks.STONE.defaultBlockState());
var layers = MineshaftHeights29.choose(List.of(new MineshaftHeights29.Probe(layered,-6,6,false)),54,16,340);
require(layers.size()==3 && layers.stream().allMatch(y->y>=186 && y<=214), "A low local surface estimate cannot send all mine trials below the real rock mass");
require(Math.abs(layers.get(0)-layers.get(1))>=8 && Math.abs(layers.get(1)-layers.get(2))>=8, "The three native heights remain distinct");
require(MineshaftHeights29.choose(List.of(new MineshaftHeights29.Probe(column(-1,384),-6,6,false)),54,16,340).isEmpty(), "Empty columns never manufacture an underground candidate");
var crossing = new MineshaftPieces.MineShaftCrossing(0,new BoundingBox(0,150,0,4,152,4),Direction.SOUTH,MineshaftStructure.Type.NORMAL);
var approach = new MineshaftPieces.MineShaftCorridor(0,RandomSource.create(42),new BoundingBox(1,150,-15,3,152,-1),Direction.NORTH,MineshaftStructure.Type.NORMAL);
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->z<0?column(140,384):column(-1,240),384)==null,
"An interior native plank crossing can join a carried corridor below a distant cavern ceiling");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->column(149,384),384)==null,
"A sturdy crossing floor needs no rock roof");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing),(x,z)->column(-1,240),384)!=null,
"An isolated crossing cannot borrow a nonexistent carried corridor");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing),(x,z)->column(149,384),384)==null,
"A wholly natural crossing floor carries itself without an invented extra corridor");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->z<0?column(140,384):column(-1,384),384)!=null,
"An open-sky crossing over void has no floor that vanilla would place");
for (var direction : List.of(Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST)) {
boolean z = direction.getAxis() == Direction.Axis.Z;
var piece = new MineshaftPieces.MineShaftCorridor(0, RandomSource.create(42),
new BoundingBox(0,150,0,z?2:14,152,z?14:2),direction,MineshaftStructure.Type.NORMAL);
var below = MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(140,180));
require(below!=null && below.size()==4 && below.stream().allMatch(b->b.minY()==140 && b.maxY()==149), "Rotated wooden feet reach real rock below");
var above = MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(-1,160));
require(above!=null && above.stream().allMatch(b->b.minY()==149 && b.maxY()==160), "Suspension-chain bounds include the real rock anchor");
var ledge = column(-1,384); ledge.setBlock(149, Blocks.STONE.defaultBlockState());
var direct = MineshaftSupport28.anchorVolumes(piece,(x,zz)->ledge);
require(direct!=null && direct.size()==4 && direct.stream().allMatch(b->b.minY()==149 && b.maxY()==149), "A sturdy natural ledge is already the native floor in every rotation");
require(!MineshaftSupport28.anchored(piece,(x,zz)->ledge), "The legacy support-only path remains unchanged");
var oneSide = MineshaftSupport28.anchorVolumes(piece,(x,zz)->
(direction.getAxis()==Direction.Axis.Z ? x==0 : zz==0) ? column(140,384) : column(-1,384));
require(oneSide!=null && oneSide.size()==2, "Native optional opposite pillars may be absent when both transverse ends remain anchored");
var firstEnd = SwampHutPlacement28.worldPosition(piece,0,-1,2);
require(MineshaftSupport28.anchorVolumes(piece,(x,zz)->
(direction.getAxis()==Direction.Axis.Z ? zz==firstEnd.getZ() : x==firstEnd.getX()) ? column(140,384) : column(-1,384))==null,
"A corridor with an entirely unattached end is still refused");
require(MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(-1,220))==null, "A completely unsupported corridor is still refused");
require(MineshaftAdmission29.intersects(above,new BoundingBox(above.getFirst().minX(),158,above.getFirst().minZ(),above.getFirst().maxX(),159,above.getFirst().maxZ())), "A secret along the chain outside the corridor body is detected");
}
System.out.println("MineshaftAdmission29Smoke: native branch volumes, central gaps, four orientations and bounded floor/chain anchors passed.");
}
private static NoiseColumn column(int floor,int ceiling) {
var states=new BlockState[384];
for(int y=0;y<states.length;y++) states[y]=(y<=floor||y>=ceiling?Blocks.STONE:Blocks.AIR).defaultBlockState();
return new NoiseColumn(0,states);
}
private static void require(boolean condition,String message) {if(!condition)throw new AssertionError(message);}
}
@@ -0,0 +1,54 @@
package fr.koka.sanctuary.worldgen;
/** Adapters and finite fractures only. Native noise presence is checked separately in Minecraft. */
public final class CavesRelief28Smoke {
public static void main(String[] args) {
for (int y = 0; y < 384; y++) {
require(CavesRelief28.deformationWeight(0, y, 0, 256) == 0, "arrival must not deform");
require(CavesRelief28.deformationWeight(256, y, 0, 256) == 0, "outer coast must not deform");
if (y <= 180) require(CavesRelief28.deformationWeight(128, y, 0, 256) == 0,
"accepted lower mass must not deform");
}
require(CavesRelief28.deformationWeight(128, 220, 0, 256)
< CavesRelief28.deformationWeight(128, 228, 0, 256), "3D deformation must have a smooth vertical envelope");
for (float v : new float[] {-10, -1, -.5F, 0, .5F, 1, 10}) {
var warp = CavesRelief28.warp(1, v, v, v);
var opposite = CavesRelief28.warp(1, -v, -v, -v);
require(Math.abs(warp.x()) <= 16 && Math.abs(warp.z()) <= 16 && Math.abs(warp.y()) <= 40,
"deformation must stay bounded in all three axes");
require(warp.x() == -opposite.x() && warp.y() == -opposite.y() && warp.z() == -opposite.z(),
"noise must permit equivalent uplift and subsidence, without positive bias");
var fixed = CavesRelief28.warp(0, v, v, v);
require(fixed.x() == 0 && fixed.y() == 0 && fixed.z() == 0, "zero envelope leaves coordinates unchanged");
}
require(CavesRelief28.warp(1, 0, .5F, 0).y() < CavesRelief28.warp(1, 0, .6F, 0).y(),
"deformation must not saturate into broad flat plateaus");
for (int y = 0; y < 384; y++) for (float cave : new float[] {-1, -.1F, .1F, 1, 64}) {
require(CavesRelief28.carve(-.3F, cave, y) == -.3F, "caves must never fill existing void");
require(CavesRelief28.carve(.7F, cave, y) <= .7F, "caves must only remove rock");
if (y <= 40 || y >= 232) require(CavesRelief28.carve(.7F, cave, y) == .7F, "bounded cave heights");
}
require(CavesRelief28.carve(.7F, -.1F, 140) < 0, "negative native cave must open solid interior");
for (long seed : new long[] {0, 42, 2026}) {
var layout = RiftShape.layout(seed);
int removed = 0, retained = 0;
for (int x = -216; x <= 216; x += 4) for (int z = -216; z <= 216; z += 4) {
require(RiftShape.density28(layout, x, 60, z, .7F, 0, 0) == .7F, "rift cannot cut lower boundary");
require(RiftShape.density28(layout, x, 260, z, .7F, 0, 0) == .7F, "rift cannot traverse upper surface");
int columnCut = 0;
for (int y = 88; y <= 244; y += 4) {
float value = RiftShape.density28(layout, x, y, z, .7F, .2F, -.3F);
require(value <= .7F, "rift cannot add material");
if (value < 0) { removed++; columnCut++; } else retained++;
}
require(columnCut < 22, "fracture cannot cut the entire vertical island mass");
}
require(removed > 0 && retained > removed, "finite ravines must open passages and retain rock");
System.out.println("caves28 rifts seed=" + seed + " removed=" + removed + " retained=" + retained);
}
System.out.println("CavesRelief28Smoke: signed 3D deformation, native cut adapter and bounded ravines passed");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,27 @@
package fr.koka.sanctuary.worldgen;
public final class River30Smoke {
public static void main(String[] args) {
for (int radius : new int[]{256,362,512}) {
PopulationHydrology.Sampler rock=(x,y,z)->Math.hypot(x,z)<radius*.82 && y>=60 && y<=230 ? 1 : -1;
var plan=River30.create(42,radius,rock);
var river=plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).findFirst().orElseThrow();
require(river.bounds().maxX()-river.bounds().minX()>radius || river.bounds().maxZ()-river.bounds().minZ()>radius,"Island-wide route");
require(plan.spills().size()>0,"A nearby edge can discharge into native void");
for(var c:plan.cells()) {
if(plan.isOutletCell(c)) {require(c.sedimentDepth()==0&&!c.hasWater(),"Outlet only removes its bank");continue;}
require(Math.hypot(c.x(),c.z())>=49,"Arrival remains natural");
for(int y=c.bedY()-c.sedimentDepth()-1;y<=c.bedY();y++) require(rock.sample(c.x(),y,c.z())>0,"Bed consists of native rock");
}
for(var spill:plan.spills()) require(Math.abs(spill.outlet().x()-spill.source().x())+Math.abs(spill.outlet().z()-spill.source().z())<=5,"Bank cut at most five blocks");
require(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
}
for(long seed:new long[]{0,42,2026,-7228211907433324401L}) {
int untouched=0;
for(int x=-350;x<=350;x+=10)for(int z=-350;z<=350;z+=10)if(Math.hypot(x,z)<350 && Relief30.influence(seed,x,z,362)==0)untouched++;
require(untouched>1000,"Wide undeformed regions remain between the three deformation areas");
}
System.out.println("River30: island-scale supported channels, five-block open banks, natural arrival, deterministic plans and separated relief regions passed.");
}
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuary.worldgen;
public final class Terraces29Smoke {
public static void main(String[] args) {
double largest = 0;
int positive = 0, negative = 0, varied = 0;
for (int y = 0; y < 384; y++) {
if (y <= Terraces29.START_Y) require(Terraces29.weight(y, 1) == 0, "lower caves stay unchanged");
require(Terraces29.weight(y, 0) == 0, "arrival and outer coast remain unchanged");
}
for (float coarse : new float[]{-1, -.5F, 0, .5F, 1})
for (float detail : new float[]{-1, -.4F, 0, .6F, 1}) {
double previous = Terraces29.sourceY(0, coarse, detail, 1);
for (int step = 1; step <= 4000; step++) {
double y = step * .1;
double mapped = Terraces29.sourceY(y, coarse, detail, 1);
double offset = mapped - y;
require(Math.abs(offset) <= Terraces29.MAX_OFFSET + 1e-10, "fine terraces stay within 2.5 blocks");
require(mapped > previous && mapped - previous < .5, "fixed local phase remains continuous and monotone");
require(Terraces29.sourceY(y, coarse, detail, 0) == y, "disabled terraces are exactly neutral");
if (offset > .5) positive++;
if (offset < -.5) negative++;
if (Math.abs(mapped - Terraces29.sourceY(y, coarse + .1F, detail - .1F, 1)) > .1) varied++;
largest = Math.max(largest, Math.abs(offset));
previous = mapped;
}
}
require(positive > 0 && negative > 0 && largest > 1.5, "fine relief has both directions at a visible but small amplitude");
require(varied > 0, "3D phase and period do not produce a common horizontal grid");
System.out.println("Terraces29Smoke: bounded local ledges, varied phase, continuous sampling and protected depths passed; max=" + largest);
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -48,12 +48,24 @@ public final class Aerial24Smoke {
&& 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(distance <= 512, "Every site has a nearby ground witness");
if (site.merchant()) {
double gap = AerialPlanner24.shoreDistance(site);
require(gap >= 16 && gap <= 48, "Complete ship envelope is 1648 blocks from its actual shore");
require(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4,
"Boarding deck remains near the shore's walking height");
} else {
require(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islets hover over the initial island");
require(site.minY() - 240 - 1 >= AerialPlanner24.MIN_ISLET_GAP,
"The unchanged rocky body has at least sixteen air blocks above the terrain");
}
require(Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) >= AerialPlanner24.ARRIVAL_RADIUS,
"Aerial bodies preserve the natural arrival zone");
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);
@@ -109,7 +121,7 @@ public final class Aerial24Smoke {
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");
"Every accepted ground witness stays outside planned root structures");
}
}
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
@@ -51,6 +51,9 @@ public final class AerialPersistence24Smoke {
require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded");
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
var root = ExpansionIsland.origin(724, SEED, 24);
require(stored.plan().sites().stream().filter(site -> site.kind() == Kind.MINERAL_ISLET)
.allMatch(site -> root.ownsChunk(site.startChunkX(), site.startChunkZ())),
"An immutable elevated islet may share initial terrain chunks without replacing root ownership");
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
byte[] pristineJournal = Files.readAllBytes(journalFile);
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
@@ -143,8 +146,8 @@ public final class AerialPersistence24Smoke {
return new AerialPlan24(List.of(
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 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)));
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -180, 270, -160, 48, 36, 48, 0, 4, -156, 240, -136, true),
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 125, 270, -170, 56, 40, 56, 0, 5, 153, 240, -142, 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); }
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.*;
/** Pure geometry controls; actual terrain, starts and walkability are checked by Transit29WorldGameTests. */
public final class Transit29Smoke {
public static void main(String[] args) {
var terrain = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return Math.hypot(x, z) < 510; }
public boolean solid(int x, int y, int z) { return y >= 55 && y <= surfaceY(x, z); }
public boolean protectedAt(int x, int y, int z) { return false; }
// This three-wide natural threshold is walkable without flattening a five-wide platform.
public int surfaceY(int x, int z) { return 250 + Math.floorMod(x, 6) / 2; }
};
for (int diameter : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42, 2026}) {
var plan = TransitPlanner29.plan(seed, 0, 0, diameter, terrain);
require(!plan.isEmpty(), "Supported hillside must admit a traversal: " + plan.diagnostic());
require(plan.equals(TransitPlanner29.plan(seed, 0, 0, diameter, terrain)), "Planning must be deterministic");
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
require(span >= diameter / 4, "A small local gallery is not the main traversal");
require(plan.modules().stream().anyMatch(m -> m.kind() == TransitPlan21.Kind.HALL), "Keep the established maintenance room where it fits");
var reached = new HashSet<TransitPlan21.Module>(); reached.add(accesses.getFirst());
for (int pass = 0; pass < plan.modules().size(); pass++) for (var a : List.copyOf(reached)) for (var b : plan.modules())
if (connected(a, b)) reached.add(b);
require(reached.size() == plan.modules().size(), "Every module and both accesses must connect through opposing doors");
}
var blocked = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return true; }
public boolean solid(int x, int y, int z) { return y <= 250; }
public boolean protectedAt(int x, int y, int z) { return true; }
public int surfaceY(int x, int z) { return 250; }
};
require(TransitPlanner29.plan(0, 0, 0, 1024, blocked).isEmpty(), "Protected fluids and structures cannot be overwritten");
var protectedBed = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return Math.hypot(x, z) < 510; }
public boolean solid(int x, int y, int z) { return y >= 55 && y <= 250; }
public boolean protectedAt(int x, int y, int z) { return y == 222; }
public int surfaceY(int x, int z) { return 250; }
};
var rescued = TransitPlanner29.plan(2026, 0, 0, 512, protectedBed);
require(!rescued.isEmpty() && rescued.diagnostic().contains("selective-footings"),
"After the complete primary search fails, valid walls may carry a route above a protected bed");
for (var m : rescued.modules()) {
require(m.minY() > 222, "The rescued route must not excavate the protected layer");
for (var support : m.supports()) require(support.minY() > 222 || support.maxY() < 222,
"No retained footing may write through the protected layer");
}
System.out.println("Transit29 pure geometry: nine seed/size cases, narrow natural thresholds, connected halls, bounded protected refusal.");
}
private static boolean connected(TransitPlan21.Module a, TransitPlan21.Module b) {
if (a.floorY() != b.floorY()) return false;
if (a.centerX() == b.centerX()) {
if (a.maxZ() + 1 == b.minZ()) return (a.openings() & 4) != 0 && (b.openings() & 1) != 0;
if (b.maxZ() + 1 == a.minZ()) return (b.openings() & 4) != 0 && (a.openings() & 1) != 0;
}
if (a.centerZ() == b.centerZ()) {
if (a.maxX() + 1 == b.minX()) return (a.openings() & 2) != 0 && (b.openings() & 8) != 0;
if (b.maxX() + 1 == a.minX()) return (b.openings() & 2) != 0 && (a.openings() & 8) != 0;
}
return false;
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.27"
version = "0.1.0-alpha.30"
pack-format = "packwiz:1.1.0"
[index]