Compare commits

...
Author SHA1 Message Date
koka 0b7e0c6717 Record alpha.30.6 publication and verified Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 19:07:46 +02:00
koka fbd870a4e3 Add underground village, branched rails and upper terrain for alpha.30.6
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 19:04:57 +02:00
koka 16e480bfd0 Record alpha.30.5 publication, scientific atlas and Prism verification
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 17:02:52 +02:00
koka 7f81bf35a0 Restore native plateaus and archive terrain comparisons for alpha.30.5
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 16:58:14 +02:00
koka e400f1e2c1 Record verified alpha30.4 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 15:58:33 +02:00
koka 2dc70fe905 Refine high plateaus and extend Sanctuary build height for alpha30.4
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 15:56:31 +02:00
koka a49dfa5337 docs: record alpha30.3 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 13:34:14 +02:00
63 changed files with 2356 additions and 57 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## 0.1.0-alpha.30.4
- Plateaux bas conservés, abords des massifs élargis et hauts reliefs en 3D ; aucun seuil de découpe latérale, microbruit ou gradin ajouté.
- Replats supérieurs proches de Y360, végétation et recherche supplémentaire de rivières courbes en altitude.
- Monde initial constructible sur 640 blocs (Y0..639), nuages inchangés et terrain toujours sous Y360.
- Conservation des secrets, traversée, navires et îlots ; aucun ajout de larène du destin.
- Nouveau monde de test nécessaire ; aucun monde personnel modifié.
## 0.1.0-alpha.30.3
- Retire les microcorniches de la 30.2, jugées trop bruitées en jeu.
+36 -2
View File
@@ -21,6 +21,40 @@ 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.6 — village souterrain et voies ramifiées
La déformation du terrain 30.5 est légèrement renforcée. Un village vanilla
abandonné dispose dune cavité locale adaptée ; les mines sont réservées à
lavance et les donjons natifs disposent de davantage de tentatives. Les voies
abandonnées sallongent et se ramifient entre les plateaux.
Deux nouveaux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand,
occupent le ciel vers Y300400. Le lieu envisagé à Y600 reste une idée future.
Voir [la génération](docs/generation-alpha30.6.md), [les essais](docs/testing-alpha30.6.md)
et [latlas scientifique](docs/terrain-atlas.md). **Nouveau monde requis.**
## Alpha.30.5 — plateaux et atlas du terrain
L’étirement vertical de la 30.4 est retiré. Les plateaux issus de la 24/27
reçoivent une déformation 3D modérée dans de larges zones ; aucune hauteur
sommitale nest imposée. Grottes, rivières courbes, brèches de cascade et
infrastructures sont conservées. Le plafond constructible reste Y640.
La comparaison devient reproductible : cartes de relief, épaisseur rocheuse
et coupes de densité des quatre champs, aux mêmes coordonnées et graines.
Voir [le terrain](docs/generation-alpha30.5.md), [loutil datlas](docs/terrain-atlas.md)
et [les vérifications](docs/testing-alpha30.5.md). **Nouveau monde requis.**
## Alpha.30.4 — massifs et ciel constructible
Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des
abords plus larges, avec des falaises et des arches issues du bruit 3D. Des replats proches de Y360 peuvent accueillir des rivières courbes.
Le plafond de construction passe à **Y640** (dernier bloc : Y639), sans relever
l’île entière ni déplacer les nuages. Les trois tailles restent inchangées.
Créer un **nouveau monde**. Voir [le terrain](docs/generation-alpha30.4.md)
et [les vérifications](docs/testing-alpha30.4.md).
## Alpha.30.3 — plateaux et grands reliefs
La passe de petites corniches de la 30.2 est retirée après le retour en jeu.
@@ -622,7 +656,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.29 |
| Sanctuary / pack | 0.1.0-alpha.30.4 |
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
@@ -651,7 +685,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.29.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.30.4.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).
+20
View File
@@ -1027,3 +1027,23 @@ part importante aux grandes déformations 3D, vers Y360. Équilibrer les trois
tailles par une distribution à l’échelle de l’île. Préserver les profondeurs,
les cours deau et les infrastructures ; limiter les petits fragments flottants.
Voir [le changement](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md).
### WG-36 — Massifs raccordés et ciel Y640 — alpha.30.4
Branche `codex/high-plateaus-alpha30-4`. Conserver les plateaux bas, retirer
les raccords trop brusques au pied des montagnes, conserver les falaises sans
petites marches répétitives ajoutées, accueillir des rivières
courbes sur les replats supérieurs et libérer de lespace constructible jusqu’à
Y639. Stabiliser un seul relief avant dajouter des personnalisations.
Larène du destin reste une idée, sans structure ajoutée.
Voir [la génération](generation-alpha30.4.md) et [les essais](testing-alpha30.4.md).
### WG-37 — Plateaux, relief 3D modéré et atlas — alpha.30.5
Branche `codex/plateau-atlas-alpha30-5`. Retirer l’étirement des coques de la
30.4 ; repartir des plateaux avec une déformation du bruit natif, sans pousser
les sommets vers Y360. Conserver les grottes, rivières courbes, sorties deau,
secrets et traversées. Comparer les champs natifs 27, 30.1, 30.4 et 30.5 par
coupes et cartes scientifiques, archivées avec leurs sources et leurs graines.
Les expéditions anciennes et les îles supérieures ciblées restent à définir.
Voir [la génération](generation-alpha30.5.md) et [latlas](terrain-atlas.md).
+54
View File
@@ -0,0 +1,54 @@
# Alpha.30.4 — massifs, hauts plateaux et ciel constructible
Le retour sur la 30.3 valide les plateaux bas et les intérieurs des montagnes,
mais rejette leurs grandes pentes étirées. Cette passe conserve un seul relief,
sans nouveau réglage de personnalisation. Petit 512, Moyen 724 par défaut et
Grand 1 024 restent disponibles dans le bouton natif Personnaliser.
## Terrain
Les provinces larges continuent de sélectionner les secteurs montagneux. La
base naturelle issue de la 24 garde sa place ; le champ rocheux relevé se
raccorde à cette base par union des volumes. Les abords des massifs s'étendent
sur 64 à 96 blocs selon la taille de l'île. L'élévation suit cette transition,
sans seuil latéral qui sectionnerait la densité en une paroi artificielle.
L'arrivée et le plateau de traversée restent préservés.
Les falaises franches sont voulues. Le retour du créateur vise les petites
marches répétitives et les lambeaux, pas toutes les parois raides. Aucun
microbruit ni gradin périodique n'est ajouté pour masquer les surfaces ; le
champ 3D conserve ses arches. Le relief inférieur à Y170 reste identique.
Le rendu des raccords reste à apprécier en jeu, particulièrement entre les
surfaces végétales et les parois rocheuses.
Les sommets suffisamment massifs portent des replats vers Y354359, avec
une altitude légèrement variable et des vides issus du champ 3D. Les sommets
ne sont pas tous forcés à cette hauteur. Le filtre des petits composants
rocheux détachés reste actif. La végétation peut maintenant s’établir sur ces
replats, avec les mêmes contrôles dappui et despace pour les arbres.
Lhydrologie cherche un cours deau supplémentaire sur les hauts plateaux,
puis conserve la recherche dune rivière sur les plateaux bas. Même tracé
courbe, mêmes lits et sédiments ; le parcours supérieur peut être plus court
pour tenir dans un massif. Sa présence dépend du terrain. Les brèches de
berges et les sources déclenchent toujours les écoulements vanilla.
## Hauteur et périmètre
Le preset actuel utilise le nouveau type `sanctuary:sanctuary_640` :
**Y0 à Y639 constructibles**, limite supérieure exclusive Y640.
Le générateur et ses trois délégués de terrain exposent la même hauteur.
Le terrain reste sous Y360 ; le nouvel espace aérien reste libre. Les nuages
restent à Y352,33. Les dimensions vanilla Nether et End ne changent pas.
Les deux navires, deux îlots, secrets, rails, traversée et expansions restent
présents. Leurs emplacements dépendent du terrain. Larène du destin est
uniquement une idée pour un futur défi en altitude : aucune plateforme,
panneau ni arène nest généré dans cette livraison.
Créer un **nouveau monde**. Conformément au workflow de tests jetables demandé,
les profils courants évoluent avec le codec `sanctuary:island_v24`. Aucun monde
personnel nest ouvert, migré ou régénéré. Ne pas prolonger une ancienne
sauvegarde pour juger les raccords de cette version.
Voir [les essais et leurs limites](testing-alpha30.4.md).
+54
View File
@@ -0,0 +1,54 @@
# Alpha.30.5 — plateaux et relief natif modéré
## Décision
La 30.4 est rejetée après essai : ses masses hautes étirent aussi les vides du
terrain, avec des coques entrecroisées peu praticables. Cette passe reprend les
plateaux de la base 24/27, les grottes et failles introduites en 28, et module
la déformation 3D native dans de grandes régions de l’île. Aucun nouveau bruit
fin de surface, cône, chambre artificielle ou quantification en étages nest ajouté.
Le déplacement dans les trois axes demeure celui de `CavesRelief28`, avec une
intensité maximale de 80 % et une répartition large calculée à l’échelle de
chaque taille. Certains secteurs gardent le plateau original. Larrivée et la
bande prévue pour la traversée conservent leur protection. Les profondeurs
jusqu’à Y170 restent identiques au champ 30.1.
Le remappage vertical vers Y360 et lunion entre terrain étiré et terrain bas
sont retirés du profil courant. Les sommets retrouvent la hauteur produite par
le champ natif ; **Y360 nest plus un objectif imposé**. Le plafond de construction
reste Y640, avec les mêmes nuages et tailles 512 / 724 / 1 024.
## Eau et exploration
La recherche des rivières revient au parcours courbe sur les surfaces
naturelles disponibles, sans priorité artificielle aux replats au-dessus de
Y330. Les brèches courtes dans les berges demeurent, avec écoulement vanilla.
Les cavités, marais, champignons, filons, secrets, rails, traversées souterraines,
navires restaurés et îlots restent dans le périmètre existant. La montgolfière
retirée depuis la 27 ne revient pas.
Le filtre borné des petits composants rocheux détachés reste actif. Il ne
prétend pas supprimer chaque fragment de chaque monde, ni les marches
inévitables de la représentation en blocs.
## Nouvelle méthode de travail
L[atlas natif](terrain-atlas.md) compare quatre champs avant décoration :
base 27 issue du retour à la 24, bruit global 30.1, étirement 30.4 et profil
30.5. Les coupes, cartes daltitude et épaisseurs sont archivées avec les données
brutes. Elles servent à examiner la géométrie avant d’évaluer le paysage en jeu.
Une métrique favorable seule ne valide pas son esthétique.
Lancienne déformation 30.4 reste disponible uniquement comme chemin de
comparaison scientifique dans le code. Le champ courant utilise toujours les
identifiants existants (`balanced_303` désigne le profil expérimental courant,
comme lors de la 30.4). Aucune migration nest réalisée : **créer un nouveau
monde**. Cette révision des profils dessai noffre pas de continuité du terrain
pour les chunks futurs dune ancienne sauvegarde.
Les îles supérieures, expansions choisies à partir de la graine et histoires
dexpédition seront définies plus tard. Aucun récit, événement ou continent
supplémentaire nest injecté dans cette livraison.
Résultats et limites : [vérifications alpha.30.5](testing-alpha30.5.md).
+97
View File
@@ -0,0 +1,97 @@
# Alpha.30.6 — village souterrain et voies ramifiées
Cette passe prolonge le terrain 30.5 validé en jeu. Elle ne reprend ni
l’étirement vertical 30.4 ni les grandes cavités artificielles de la 25.
Créer un **nouveau monde** : le profil de prototype `sanctuary:island_v24`
évolue avec laccord du créateur pour ses mondes de test. Plafond Y640,
Minecraft 26.3-pre-2, Petit 512 / Moyen 724 par défaut / Grand 1 024 inchangés.
## Relief
Lamplitude de la déformation 3D supérieure passe de 0,80 à 0,95 dans les
mêmes grandes provinces. Leur répartition, les plateaux, les grottes basses,
les rivières courbes et le filtrage des petits fragments restent en place.
Il ny a ni sommet imposé ni ajout de bruit de détail. Latlas compare les
champs 24/27, 30.1, 30.5 et 30.6 aux mêmes coordonnées.
Deux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand, sont
recherchés entre Y300 et Y400 au-dessus de l’île. Ce sont des volumes rocheux
pleins érodés par un bruit 3D large, indépendants des deux îlots minéraux à
coffre. Ils ne reçoivent aucun butin ni construction. Leurs positions sont
séparées et recherchées en 24 essais au plus par masse ; le champ de terrain
existant est sondé pour conserver un espace dair dessous. Le filtrage des
petits fragments sapplique également à ces hauteurs. La surface projetée
reste minoritaire ; aucun semis de blocs flottants nest ajouté.
Lidée dun lieu exceptionnel vers **Y600** est conservée pour plus tard.
Aucune structure, plateforme ou arène ny est générée dans cette version.
## Village abandonné
Un village vanilla abandonné est recherché dans la roche de l’île initiale.
**La cavité est aménagée spécialement pour lui**, à la demande du créateur :
sol commun praticable, chambres qui se recouvrent autour du graphe complet,
voûte irrégulière et accès vers les grottes voisines. Ce creusement reste
local ; aucun nouveau champ de cavités ne s’étend à l’île entière.
- Identifiant : `sanctuary:underground_abandoned_village`.
- Assemblage Jigsaw natif des plaines abandonnées, taille 6 et distance 80 ;
ces paramètres ne fixent pas un nombre de maisons. Aucun pool Minecraft
global nest remplacé, aucun graphe nest réduit pour le faire tenir.
- Au moins six maisons pour accepter un graphe. Les templates natifs gardent
leurs lits, coffres, toiles et zombie-villageois persistants et guérissables.
- Les rues utilisent le même sol pendant la planification **et** la pose,
sans être projetées sur le toit de l’île.
- Au plus 60 positions × six altitudes possibles, au plus 32 000 colonnes mises en
cache ; aucun chargement de chunks pour rechercher lemplacement.
- Le sous-sol doit offrir une majorité dappuis et de couverture. Le plan
local évite les fluides détectés, les secrets et les infrastructures ; les
raccords sont contrôlés avec leur emprise complète. Les refus sont journalisés.
- Les pièces enregistrent la cavité avec les maisons ; le locator et le
registre observent le véritable départ. Le village ne se reconstitue pas
dans un chunk déjà généré.
Le creusement laisse des fenêtres naturelles possibles. Ce nest pas une
nouvelle ville de surface, un laboratoire ou une restauration des Lost Cities.
Les options expérimentales et générales de structures doivent être actives.
## Mines et donjons
Avant les premiers chunks, le générateur réserve des mines Minecraft complètes :
objectif **une sur Petit/Moyen, deux sur Grand**. Il cherche dans au plus 192
candidats natifs, avec les contrôles dappuis et daltitude existants. Le plan
accepté est celui effectivement placé et localisé. Un échec reste explicite
au journal ; les graines testées ne prouvent pas lensemble des graines possibles.
Les mines restent indépendantes du commutateur expérimental ; loption générale
« Générer les structures » fait autorité.
Les salles à spawner restent la feature vanilla `monster_room`, avec ses
contrôles, butins et mobs. Les tentatives explorent neuf colonnes par chunk
au lieu de quatre, dans chaque chunk éligible, avec au plus 32 tentatives.
Les pièces protégées et les fluides restent exclus. Les plantes dont le support
est retiré par la pose tardive dun donjon sont nettoyées dans son volume, sans
modifier les plantes valides ni produire de drops. Cela sapplique aussi
aux expansions du générateur courant ; aucune expansion nest ouverte par
la recherche de structures.
## Voies de surface
Les voies se développent en longues portions suivant les pentes admissibles,
avec des bifurcations au milieu des parcours et des prolongements. Le ballast
reste continu sur les portions retenues ; certains rails et aiguillages sont
manquants et réparables. Aucune gare nest nécessaire pour conserver une voie.
Larrivée naturelle reste protégée sur 48 blocs autour de lorigine.
Les **68 % évoqués sont un repère visuel, pas un quota imposé**. Latlas des
structures mesure les colonnes aménagées et les superpose au relief. Les voies
sarrêtent lorsquun terrain ou un raccord ne convient pas ; elles ne terrassent
pas une autoroute à travers les montagnes ou le vide pour atteindre un quota.
## Registre et vérification
Une erreur darrêt révélée par les essais est corrigée : Minecraft peut exécuter
une tâche immédiatement sur un worker lorsque le serveur sarrête. Le registre
ne tente plus de modifier son état depuis ce worker. Les chunks trop tardifs
restent inconnus jusqu’à leur prochain chargement, sans inventer une absence.
Voir [les essais et limites](testing-alpha30.6.md) et [la méthode datlas](terrain-atlas.md).
+139
View File
@@ -13,6 +13,145 @@ 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.30.6 publiée — 12 septembre 2026
La [release alpha.30.6](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.6)
est publiée depuis `fbd870a4e3522704e35f41606785f90b1b1eb764`, branche
`codex/underground-exploration-alpha30-6`. Village vanilla abandonné dans une
cavité locale, mines réservées, donjons natifs plus fréquents, voies ramifiées,
relief légèrement renforcé et quelques masses naturelles vers Y300400.
Aucun lieu à Y600. **Nouveau monde requis.**
`check build` réussit avec **25/25 tests natifs sur Moyen 724 / graine 0**.
Le Grand de la graine `-7228211907433324401` a passé 25/25 tests avant
lultime sculpture aérienne, puis 2/2 tests de plans sur le code final ; village
et deux mines conservent leurs emprises. Neuf relevés natifs composent latlas.
Lassemblage est réussi avec le contrôle déjà exécuté exclu de sa répétition.
Voir [les résultats, commandes et limites](testing-alpha30.6.md).
Artefacts publics vérifiés :
- [MRpack alpha.30.6](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-0.1.0-alpha.30.6.mrpack).
- [Atlas scientifique complet, PNG/PDF/JSON](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-Atlas-0.1.0-alpha.30.6.zip).
- [Coupes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-0.1.0-alpha.30.6-Grand-coupes.png), [cartes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-0.1.0-alpha.30.6-Grand-cartes.png).
- [Masses aériennes du Moyen](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-0.1.0-alpha.30.6-Moyen-terrains-aeriens.png), [coupes du village du Moyen](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.6/Sanctuary-0.1.0-alpha.30.6-Moyen-village.png).
Canal packwiz : `b4a3da5c0a4e713c97ec863bcbc470e0df505f90`.
Minecraft 26.3-pre-2, Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta
sur Mac**, jeu fermé, réussissent. Les **925 fichiers personnels et réglages**
suivis conservent leurs empreintes. Le dossier reste `Sanctuary-0.1.0-alpha.1`.
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30.6/`.
SHA-256 JAR : `107006ca40143fa0bd7495043a7af887420844fd1d584fe132f920aed3436271`.
SHA-256 MRpack : `15853900fc772ffc1fad35235085131176b5319d72948a5c67fabeedf1396322`.
SHA-256 atlas : `7ffc4cd6adea67b6e7e3e2ebfd9f5f60fec8dbe41ab080f689f3067b6ffe5522`.
Reçus : `build/alpha306-delivery-public.json`, `build/alpha306-isolated-validation.json`,
`build/alpha306-prism-validation.json` et `build/alpha306-atlas-archive.json`.
Aucun essai ni déploiement Windows natif nest revendiqué.
## Alpha.30.5 publiée — 12 septembre 2026
La [release alpha.30.5](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.5)
est publiée depuis `7f81bf35a0cb3d71527e3795cdf22cff55b7c8e8`, branche
`codex/plateau-atlas-alpha30-5`. L’étirement vertical est retiré au profit des
plateaux et dune déformation 3D modérée. Aucun sommet Y360 nest imposé ;
le plafond constructible reste Y640. Nouveau monde requis.
`check build` et `assemblePack` réussissent. Grand 1 024, graine
`-7228211907433324401`, puis Moyen 724, graine 0 : **24/24 tests natifs chacun**.
Les neuf relevés de densité couvrent les trois tailles sur les graines 0, 42
et la graine du Grand. Voir [les résultats et limites](testing-alpha30.5.md).
Artefacts publics vérifiés :
- [MRpack alpha.30.5](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-0.1.0-alpha.30.5.mrpack).
- [Atlas scientifique complet, PNG/PDF/JSON](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-Atlas-0.1.0-alpha.30.5.zip).
- [Coupes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-0.1.0-alpha.30.5-Grand-coupes.png) et [cartes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-0.1.0-alpha.30.5-Grand-cartes.png).
Canal packwiz : `bdaf594f61601f5ea0b76b7e4e19b0cd3b35e04b`.
Minecraft 26.3-pre-2, Loader 0.19.5, Fabric API 0.160.0+26.3 inchangés.
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta
sur Mac**, jeu fermé, réussissent. Les **925 fichiers personnels et réglages**
suivis conservent leurs empreintes. Le dossier reste `Sanctuary-0.1.0-alpha.1`.
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30.5/`.
SHA-256 JAR : `0f26ef0940234a241bb4df3b4585ac15d04cfa858ca47db1ce3d4802f49a35a5`.
SHA-256 MRpack : `0f76c85f77f981951642449d5b51bf0818ed31f3a826e6f3c9deeec6f54868d4`.
SHA-256 atlas : `3da0d0b2422453689c403ca49d66cb54727948259fd0b791d54d233298a05ef2`.
Reçus : `build/alpha305-delivery-public.json`, `build/alpha305-isolated-validation.json`,
`build/alpha305-prism-validation.json` et `build/alpha305-atlas-archive.json`.
Aucun essai ni déploiement Windows natif nest revendiqué.
## Alpha.30.4 publiée — 12 septembre 2026
La [release alpha.30.4](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.4)
est publiée depuis `2dc70fe9051ab29996f1047bee6f15b6048910bb`, branche
`codex/high-plateaus-alpha30-4`. Elle élargit les abords des massifs, conserve
les plateaux bas et cherche des rivières sur les replats supérieurs. Le monde
est constructible sur Y0..639 ; terrain sous Y360 et nuages inchangés.
Les falaises sont conservées. Certaines faces inclinées restent en marches :
leur disparition complète n'est pas revendiquée.
`check build` et `assemblePack` réussissent. Les mondes Moyen 0 et Grand
`-7228211907433324401` passent chacun 24 tests ; le Grand est rechargé dans
un second processus, 24/24, avec maintien du bloc à Y639 et des modifications
faites aux satellites. Le client natif valide les menus, le Blocodex et le ciel,
et fournit deux captures réelles. Voir [le terrain](generation-alpha30.4.md)
et [les preuves et limites](testing-alpha30.4.md).
Le [MRpack alpha.30.4](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.4/Sanctuary-0.1.0-alpha.30.4.mrpack),
le JAR et le ZIP d'amorçage sont vérifiés localement puis par téléchargement public.
Canal packwiz : `664b0c687647ef895c0ffd2b26fa28d7edf1bd34`.
Minecraft 26.3-pre-2, Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta
sur Mac**, jeu fermé, réussissent. Un seul JAR Sanctuary 30.4 est actif ; les
**925 fichiers personnels et réglages** suivis conservent leurs empreintes.
Le dossier reste `Sanctuary-0.1.0-alpha.1`. Sauvegarde ciblée :
`sanctuary-backups/before-0.1.0-alpha.30.4/`.
SHA-256 JAR : `9f03c1ac7c2adc085f2df1b54abb3903c8832b68df1d19ba530594ea97bf1a92`.
SHA-256 MRpack : `8342a4036360416b291b82ef01ec84b273673dfdf7899fdde9c2de7b84bffc79`.
Reçus : `build/alpha304-delivery-public.json`, `build/alpha304-isolated-validation.json`,
`build/alpha304-prism-validation.json`.
Créer un **nouveau monde**. Aucun essai ou déploiement Windows natif, ni
validation esthétique par le créateur, n'est revendiqué.
## Alpha.30.3 publiée — 12 septembre 2026
La [release alpha.30.3](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.3)
est publiée depuis `076c9a6c16e93e46db4a16ed0dd8d6a5a22c5a85`, branche
`codex/balanced-relief-alpha30-3`. Elle retire les microcorniches de la 30.2,
mélange plateaux naturels et grandes provinces montagneuses en 3D et conserve
le filtrage des petits fragments. Les recherches aériennes et le veto de biome
du sanctuaire dans les marais sont corrigés.
`check build` et `assemblePack` réussissent. Les trois profils sont comparés
sur les graines 0, 42 et `-7228211907433324401`. Trois mondes natifs ont terminé
à 24/24 tests ; Petit 42 et Grand signalé comprennent les correctifs finaux de
placement. Voir [le terrain](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md).
Le [MRpack alpha.30.3](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.3/Sanctuary-0.1.0-alpha.30.3.mrpack),
le JAR et le ZIP damorçage ont été vérifiés localement et par téléchargement public.
Canal packwiz : `a68fe0df2f82868100219799db603805f89c56c7`.
Minecraft 26.3-pre-2, Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
Deux installations isolées puis deux synchronisations de la même instance
**Sanctuary Beta sur Mac**, jeu fermé, ont réussi. Un seul JAR Sanctuary 30.3
est actif ; les 925 fichiers personnels et réglages suivis gardent leurs empreintes.
Le dossier dinstance reste `Sanctuary-0.1.0-alpha.1`. Sauvegarde ciblée :
`sanctuary-backups/before-0.1.0-alpha.30.3/`.
SHA-256 JAR : `6713622a4ad8034bb230ff11e1bad580d218932da31b3feeddee4c905fd11759`.
SHA-256 MRpack : `bb1172d14a8092d8155edeb61dba943c0a149874580ab4bd21abd8e87db54255`.
Reçus : `build/alpha303-delivery-public.json`, `build/alpha303-isolated-validation.json`,
`build/alpha303-prism-validation.json`.
Créer un **nouveau monde Sanctuary**. Aucun essai natif Windows, mise à jour
Windows ou validation esthétique par le créateur ne sont revendiqués.
## Alpha.30.2 publiée — 12 septembre 2026
La [release alpha.30.2](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.2)
+102
View File
@@ -0,0 +1,102 @@
# Atlas scientifique du terrain
L[archive alpha.30.5](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-Atlas-0.1.0-alpha.30.5.zip)
contient les neuf cas, leurs figures PNG/PDF et données brutes.
[Coupes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-0.1.0-alpha.30.5-Grand-coupes.png) ·
[Cartes du Grand](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.5/Sanctuary-0.1.0-alpha.30.5-Grand-cartes.png).
## Acquisition native
`TerrainAtlas305GameTests` charge les registres réels de Minecraft 26.3-pre-2
et les ressources Sanctuary. Il crée le même `RandomState` que le générateur
pour chaque graine et taille. Il compile les fonctions de densité natives,
sans réimplémentation du bruit en Python. Le serveur de test utilise un monde
plat ignoré pour démarrer rapidement ; aucun chunk Sanctuary ni aucune
expansion nest créé pour cet export.
```sh
./gradlew :sanctuary:runGameTest -PsanctuaryAtlasOnly=true \
-PsanctuaryAtlasSeeds=0,42,-7228211907433324401 \
-PsanctuaryAtlasRevision=REVISION_DU_CODE
python3.14 -m venv build/atlas-venv
build/atlas-venv/bin/pip install -r scripts/terrain-atlas-requirements.txt
build/atlas-venv/bin/python scripts/terrain_atlas.py --output build/atlas306 \
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-*.json
```
Le tracé livré utilise Python 3.14 ; les versions de Matplotlib et NumPy
sont épinglées dans le fichier de dépendances.
Les propriétés atlas sont réservées aux tests. Les tests de génération complets
restent actifs dans `check` sans ce paramètre. Ne pas lancer deux Gradle de test
simultanément : leur dossier de monde temporaire est partagé. Les diagnostics
existants peuvent rester dans le dossier ; vérifier graines, version et provenance
avant den faire une archive. Aucune sauvegarde personnelle nest utilisée.
## Lecture des planches
- Cartes : nord en haut, X et Z en blocs, rapport daspect réel. Les quatre
profils partagent les mêmes échelles de couleur. La palette daltitude ne
représente pas les blocs ou les biomes ; du blanc nimplique pas de neige.
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
Une cellule grise signifie quaucun solide na été échantillonné dans la colonne.
- Épaisseur cumulée : nombre d’échantillons solides multiplié par 2 sur Y2..418 dans la 30.6 (Y2..382 dans la 30.5).
Ce nest ni une densité physique, ni un volume exact de matériaux minables.
- Roche continue : épaisseur depuis le sommet jusquau premier vide échantillonné.
Un petit résultat peut signaler une voûte mince, un surplomb ou un fragment.
- Coupes : trois plans Z=0, Z=96 et Z≈−rayon/4, sur toute la hauteur naturelle.
Bleu pour la densité négative, rouge pour la positive ; frontière zéro en noir.
L’échelle colorée sature au-delà de ±0,3 ; les données conservées vont jusqu’à ±1.
Le pas horizontal des cartes est de 12 blocs, le pas vertical de 2. Les coupes
sont échantillonnées tous les 2 blocs dans les deux axes. Les images ne sont
pas lissées. Le champ est quantifié à 0,0001 en conservant son signe autour de
zéro. Les petits détails peuvent être manqués : le contrôle des fragments en
volume utilise séparément une résolution dun bloc dans les tests complets.
Leau, la lave, les matériaux de surface, végétaux, biomes et structures ne
figurent pas dans cet atlas de **densité avant décoration**. Les tests natifs
de rivières et de structures vérifient leur présence effective dans des chunks.
Une coupe 2D ne permet pas, seule, de conclure à la connectivité dune grotte 3D.
## Comparaison et conservation
Les tableaux utilisent les mêmes coordonnées dans chaque variante. La part
des plateaux conservés compare les colonnes de base dont le sommet atteint
Y200, avec une tolérance de deux blocs. Les statistiques d’épaisseur du toit
utilisent uniquement les colonnes atteignant Y200 dans **les quatre variantes**.
Ce sous-ensemble ne représente pas toutes les falaises de l’île.
Larchive de livraison contient JSON natifs, PNG, PDF, résumés, script de rendu,
versions Python et empreintes des fichiers. Elle est attachée à la release,
pour que les images restent récupérables même après nettoyage du dossier build.
Le manifeste relie les données au binaire livré et à la révision source.
Les vues sont générées par Matplotlib à partir des mesures ; aucune image
générative nintervient.
Le libellé « 24 / 27 » désigne le champ de la 27 restauré depuis la 24, pas
une simulation de tous les matériaux et structures historiques de la 24.
Les samplers 30.1 et 30.4 sont conservés pour comparer des transformations
exactes, et non des approximations dessinées pour illustrer un résultat attendu.
## Comparaison 30.6 et infrastructures
Lexport courant remplace la colonne 30.4 par la comparaison directe 30.5/30.6.
`compileHistorical305` conserve exactement le coefficient 0,80 ; le champ livré
utilise 0,95. Le renderer accepte également les anciens exports 30.5.
Des coupes supplémentaires Y260..418 ciblent les morceaux de terrain supérieurs.
Les JSON courants sont dans `diagnostics/atlas306/`.
`scripts/structure_atlas.py` superpose les plans ferroviaires au relief et trace
les coupes des blocs du village effectivement générés par `Village306GameTests`.
Le pourcentage est une estimation de surface projetée à partir du pas de
12 blocs ; le numérateur inclut les colonnes de ballast et les chemins. Ce nest
ni une longueur de rails en pourcentage ni une part du volume de l’île.
```sh
build/atlas-venv/bin/python scripts/structure_atlas.py \
mods/sanctuary/build/run/gameTest/diagnostics/village306/724-0.json \
mods/sanctuary/build/run/gameTest/diagnostics/atlas306/terrain-724-0.json \
--output build/atlas306/structures
```
+8
View File
@@ -95,3 +95,11 @@ Essais exécutés sur Mac ; aucun essai natif Windows par lagent. Les variati
de relief exigent un nouveau monde ; aucun monde personnel nest régénéré.
Le filtrage local de connexité nest pas un audit exhaustif de l’île entière.
La présence des mines, huttes et donjons reste conditionnelle au terrain.
## Artefacts et installation
Le JAR, le MRpack et le ZIP Prism ont été contrôlés localement puis téléchargés
publiquement et comparés octet par octet. Deux synchronisations isolées, puis
deux dans la même instance Prism Mac, réussissent. Les 925 fichiers personnels
et réglages suivis conservent leurs empreintes ; un seul JAR Sanctuary 30.3 est actif.
Les reçus et empreintes sont consignés dans [packwiz](packwiz.md).
+92
View File
@@ -0,0 +1,92 @@
# Vérifications alpha.30.4
Essais natifs sur Mac, Minecraft 26.3-pre-2, Java 25. Aucun essai Windows
natif. Les visualisations de densité ne sont pas des captures Minecraft et
ne constituent pas une validation esthétique du paysage décoré.
## Itérations écartées
Les premiers essais Moyen 0 (PID 71154) et Petit 42 (PID 71587) avaient passé
24 tests chacun. Leur retrait latéral produisait toutefois une transition trop
brutale : cette version a été écartée après le retour visuel du créateur.
Leurs mesures de surface ne décrivent donc pas le binaire final.
Le contrôle Grand a ensuite détecté une régression de la densité profonde
lors d'un raccourci de calcul. Le remappage vertical a été corrigé pour rester
strictement identique sous Y170 ; le contrôle n'a pas été affaibli.
## Terrain corrigé
Moyen 724, graine 0, PID 72270 : **24/24 tests natifs**, 4 min 46 s Gradle.
Quatre lieux aériens, quatre coffres et deux villageois. Traversée de 272 blocs,
19 départs, 29 chunks FULL ; sanctuaire minéral présent. Deux rivières courbes,
à Y347 et Y243, un lac à Y250. Sept cellules d'eau coulante observées sous
les brèches ; les surfaces d'eau sont testées sous ticks réels.
Les trois tailles sont échantillonnées dans le même essai : sommets à
Y354/356/354. Respectivement 61/54/59 % des colonnes supérieures comparées
restent à deux blocs au plus de la base 24. Cela conserve des secteurs bas,
sans prétendre qu'il s'agit d'une mesure exhaustive de la surface.
Le volume de contrôle passe de cinq petits composants rocheux détachés à zéro ;
1 536 points sont identiques en ordre inverse. Les 5 819 échantillons profonds
restent identiques. Ces relevés ne garantissent pas chaque bloc de chaque monde.
Le bloc à Y639 est accepté, Y640 refusé ; l'espace supérieur reste initialement vide.
Preuves locales ignorées : `build/alpha304-evidence/medium-0-current/`.
## Client et validation finale
Le client natif valide les mixins de ciel/brouillard, les trois tailles dans
Personnaliser (Valider, Annuler, Échap), le Blocodex et ses échanges réseau.
Le premier protocole de captures a produit des vues inutilisables : il
confondait l'existence théorique d'un chunk client avec sa réception réelle.
Ces premières images ne constituent pas une validation visuelle. Le protocole
corrigé attend de vrais chunks reçus, la caméra et le rendu. Le client passe
en 2 min 20 s (`build/alpha304-client-received-chunks.log`) et fournit deux
captures réelles, sans shaders, distance de rendu 8 et nuages désactivés par
le harnais. Une arche, ses abords végétalisés et une rivière haute avec sa
cascade sont visibles. Des marches subsistent sur certaines faces inclinées :
ce défaut visuel n'est pas déclaré éliminé partout.
Captures locales : `build/alpha304-evidence/client/`. La seconde montre le
massif photographié avec du brouillard de distance ; elle ne représente pas
l'ensemble de l'île ni une validation exhaustive de ses raccords.
`./gradlew check build` réussit en 7 min 53 s sur le terrain final.
Grand 1 024, graine `-7228211907433324401`, PID 73175 : **24/24**.
La traversée mesure 374 blocs, avec 23 départs et 52 chunks FULL.
Quatre lieux aériens, quatre coffres, deux villageois et le sanctuaire minéral
sont présents. Une rivière courbe et huit cellules coulantes sous ses sorties
sont vérifiées. Sur cette graine, une deuxième rivière haute n'est pas admise.
Le plafond Y639 est constructible ; Y640 est refusé.
Les profils 512/724/1024 échantillonnés sur cette graine culminent à
Y330/354/354 ; les maxima ne sont pas imposés à chaque taille. Le volume
contrôlé passe de 12 petits composants détachés à zéro. Les 7 436 échantillons
profonds sont identiques ; 1 536 points restent identiques en ordre inverse.
Preuves : `build/alpha304-check-build-verified.log` et
`build/alpha304-evidence/large-user-current/`.
Un second processus natif, PID 73544, recharge le même Grand : **24/24**
en 3 min 12 s. Le bloc posé à Y639 persiste, les coffres pillés et les
satellites partiellement démolis restent dans cet état ; les occupants déplacés
ne sont pas recréés. Les départs, références et marqueurs d'habitants sont
identiques, le journal d'expansion inchangé.
Preuves : `build/alpha304-reload-large.log` et
`build/alpha304-evidence/large-user-reload/reload1.json`.
`assemblePack` réussit après `check build`, sans relancer les mêmes tests.
Le JAR, le MRpack et le ZIP d'amorçage sont vérifiés localement : versions,
Minecraft exact, codec, hauteur Y640, dépendances épinglées et intégrité ZIP.
Les téléchargements publics sont identiques aux artefacts locaux. Deux
synchronisations isolées, puis deux dans la même instance Prism Mac, jeu fermé,
réussissent. Les 925 fichiers personnels et réglages suivis gardent leurs
empreintes. Reçus et empreintes dans [la distribution packwiz](packwiz.md).
## Limites
Les rivières restent conditionnelles : un sommet étroit ne reçoit pas un
cours d'eau forcé. Le contrôle des cascades observe leur écoulement local,
pas une simulation exhaustive de chaque chute jusqu'à Y0. Les études de
terrain couvrent des échantillons, et les tests de structures chargent des
emprises ciblées. Aucun monde personnel n'est utilisé. Nouveau monde requis.
+96
View File
@@ -0,0 +1,96 @@
# Vérifications alpha.30.5
Essais natifs sur **Mac**, Minecraft 26.3-pre-2 et Java 25. Aucun essai Windows
natif. Latlas est une mesure du champ de terrain avant décoration, pas une
capture du rendu Minecraft ni une validation esthétique du créateur.
## Atlas natif
Trois tailles × trois graines, quatre champs comparés par cas : base 27,
global 30.1, étirement 30.4 et profil 30.5. Les deux acquisitions atlas passent
chacune les deux tests requis du serveur plat. Aucun chunk Sanctuary nest
généré pour cette acquisition. Les neuf exports prennent environ 3 min 30 s
de calcul natif au total, hors démarrage et tracé des planches.
| Diamètre | Graine | Sommet échantillonné 30.5 | Plateaux conservés à ±2 blocs |
|---|---|---|---|
| 512 | `-7228211907433324401` | Y258 | 82.7 % |
| 724 | `-7228211907433324401` | Y276 | 77.6 % |
| 1024 | `-7228211907433324401` | Y274 | 73.9 % |
| 512 | `0` | Y272 | 84.4 % |
| 724 | `0` | Y272 | 80.9 % |
| 1024 | `0` | Y272 | 79.1 % |
| 512 | `42` | Y270 | 74.3 % |
| 724 | `42` | Y278 | 70.7 % |
| 1024 | `42` | Y278 | 71.1 % |
« Plateaux » signifie ici les colonnes de base dont le sommet atteint Y200,
sur une grille horizontale de 12 blocs et verticale de 2 ; il ne sagit pas
dun inventaire de chaque bloc. Les coupes sont tracées avec des proportions
réelles et des échelles communes, sans interpolation daffichage.
L’épaisseur du toit ne saméliore pas partout par rapport à la 30.4 : la part
des toits de huit blocs ou moins passe notamment de 10,2 à 11,5 % dans le
Grand 0, sur les colonnes supérieures communes. Le changement recherché est
le retrait de l’étirement et des grandes coques hautes, **pas le remplissage
des cavités naturelles**. Les rapports complets conservent ces contre-indications.
Le calcul historique 30.4 et le calcul 30.1 sont comparés au relevé de la
30.4 livrée, Moyen 0, aux mêmes coordonnées : **79 860 signes de densité
identiques par variante, aucune différence**. Les témoins historiques de
latlas correspondent donc aux archives effectivement produites précédemment.
Reçu local : `build/alpha305-historical-equivalence.json`.
Larchive contient neuf paires de planches PNG/PDF, les neuf JSON natifs,
leurs résumés et empreintes, le script Matplotlib et ses dépendances.
Voir [la méthode datlas](terrain-atlas.md).
## Validation des chunks
`./gradlew check build` réussit en **7 min 32 s**. Les vérifications complètes
utilisent le Grand 1 024, graine `-7228211907433324401`, PID 74819 : **24/24**.
- Traversée de 374 blocs, 23 départs locaux, 52 chunks FULL ; un parcours
effectif de 532 pas relie ses deux accès en respectant le volume de collision
dun joueur. 71 cibles de passage sont accessibles.
- Sanctuaire minéral : départ natif, sol central et espace du gardien présents.
- Rivière courbe à Y243 ; huit cellules deau observées sous les ouvertures
après ticks vanilla. Les témoins du lit conservent leur eau.
- Quatre lieux aériens, quatre coffres, deux villageois ; la cascade de l’îlot
et sa végétation sont vérifiées dans les chunks générés.
- Le bloc à Y639 est accepté, Y640 refusé. Le journal dexpansion ne change pas.
- 7 436 échantillons de densité profonde sont identiques à la 30.1. Le volume
de contrôle passe de 12 petits composants détachés à zéro ; 1 536 points
restent identiques en ordre inverse de lecture.
Preuves : `build/alpha305-check-build.log` et `build/alpha305-evidence/large-user/`.
Le **Moyen 724, graine 0**, PID 75795, passe également **24/24** en 4 min 27 s.
Sa traversée de 272 blocs compte 19 départs et 30 chunks FULL, avec un parcours
de 426 pas entre les accès. Le sanctuaire minéral et les quatre lieux aériens
sont présents, avec quatre coffres et deux villageois. Rivière et lac restent
en eau à Y247 et Y251. Aucune brèche de berge nest retenue par le plan sur
ce témoin : aucune cascade fluviale nest donc revendiquée pour ce cas.
La cascade de l’îlot reste contrôlée séparément.
Les 5 819 échantillons profonds restent identiques ; le volume ciblé passe
de cinq petits composants détachés à zéro, avec 1 536 points identiques en
ordre inverse. Preuves : `build/alpha305-medium0.log` et
`build/alpha305-evidence/medium0/`.
`./gradlew assemblePack -x :sanctuary:check` réussit après le `check build`,
sans répéter les contrôles déjà passés. Le JAR, le MRpack et le ZIP damorçage
sont vérifiés localement : versions, Minecraft exact, codec, hauteur de 640,
dépendances épinglées et intégrité des archives. Les téléchargements publics
et la synchronisation Prism font lobjet de reçus de distribution séparés
référencés dans [packwiz](packwiz.md).
## Limites
Les coupes ne montrent ni la végétation, ni leau, ni les structures. Elles ne
prouvent pas la connectivité 3D des vides. Les grottes naturelles peuvent
encore produire des voûtes minces et des arches. Les tests de fragments portent
sur un volume ciblé à résolution dun bloc, pas sur chaque bloc du monde.
Aucun test de rendu client ou shader nouveau nest revendiqué dans cette passe.
Les contrôles fonctionnels du Blocodex sont côté serveur ; son interface na
pas été modifiée. Aucun essai Windows, aucune activation dexpansion ni
modification dune sauvegarde personnelle. Créer un nouveau monde.
+108
View File
@@ -0,0 +1,108 @@
# Vérifications alpha.30.6
Essais natifs sur **Mac**, Minecraft 26.3-pre-2, Java 25. Aucun essai Windows
natif nest revendiqué. Un **nouveau monde** est nécessaire.
## Atlas final
Neuf couples : Petit 512 / Moyen 724 / Grand 1024 × graines 0, 42 et
-7228211907433324401. Le serveur dexport plat passe ses deux tests ;
lacquisition finale dure 3 min 37 s, démarrage compris. Elle compile le champ
natif avant eau, matériaux, végétation et structures, sans générer de chunks
Sanctuary. Le rendu utilise Matplotlib/NumPy épinglés et aucune interpolation.
Les trois références (24/27, 30.1, 30.5) sont comparées aux neuf exports
archivés de la 30.5 : colonnes et valeurs de densité des coupes **identiques**.
Laugmentation du domaine daffichage à Y420 ajoute seulement de lair aux
références ; le plafond constructible reste Y640.
| Diamètre | Graine | Sommet échantillonné, formes aériennes incluses | Hauts plateaux conservés à ±2 blocs |
|---|---|---|---|
| 1024 | `-7228211907433324401` | Y382 | 71.6 % |
| 1024 | `0` | Y374 | 75.9 % |
| 1024 | `42` | Y386 | 68.8 % |
| 512 | `-7228211907433324401` | Y378 | 78.7 % |
| 512 | `0` | Y376 | 78.8 % |
| 512 | `42` | Y384 | 69.9 % |
| 724 | `-7228211907433324401` | Y384 | 76.5 % |
| 724 | `0` | Y376 | 77.6 % |
| 724 | `42` | Y382 | 68.0 % |
La dernière colonne compare les colonnes de la base 24/27 atteignant Y200,
sur un pas horizontal de 12 blocs et vertical de 2. Elle ne mesure pas toute
la surface au bloc près. Les coupes supplémentaires ciblent les masses
supérieures ; une coupe 2D ne démontre pas à elle seule une connexion en 3D.
Le filtrage des composantes de 32 blocs ou moins reste actif dans le volume.
## Essais de structures
La passe complète du Grand sur la graine utilisateur a passé les 25 GameTests
et `check build` (9 min 58 s). Le village contient 12 maisons natives,
20 blocs de lits, 3 coffres, 852 blocs de rue et 9 zombie-villageois persistants,
observés dans 84 chunks FULL. Deux départs de mines vanilla sont réservés.
La recherche native par identifiants trouve une vraie mine au chunk [13,-2]
sans ouvrir dexpansion ; ses corridors et leurs éléments sont inspectés.
93 salles vanilla `monster_room` ont été observées dans lensemble de cette
passe : ce nest pas le nombre total de donjons de l’île ni une garantie par graine.
Ce témoin complet précède lultime sculpture des morceaux de terrain aérien.
La génération sous Y300 na pas changé dans ce dernier ajustement. Les plans
finaux du Grand et le témoin complet du Moyen sont vérifiés séparément ci-dessous.
La cavité et toutes les pièces font un aller-retour NBT : position, rotation,
modèle, projection et jonctions conservés. Sur le Grand, Minecraft réduit la
marge verticale de collision de 18 pièces Jigsaw à la boîte du template lors
du chargement ; le test vérifie précisément cette normalisation native. Ce
contrôle ne remplace pas un essai de pillage suivi dun redémarrage complet.
Les premières passes de développement avaient aussi observé 17 maisons,
3 coffres et 6 zombie-villageois sur Petit/42, et 15 maisons, 2 coffres et
10 zombie-villageois sur Moyen/0. Elles ne sont pas présentées comme des
essais complets du dernier binaire.
## Témoin final Moyen / 0
Le dernier `./gradlew check build`, après correction des plantes privées de
support par les donjons, réussit en **7 min 20 s** : **25/25 GameTests**, puis
tous les contrôles de build. Ce temps inclut le démarrage et les tests ; ce
nest pas une mesure du temps douverture dun monde chez un joueur.
Le village réellement généré comporte **15 maisons natives, 24 blocs de lits,
2 coffres, 1 340 blocs de rue et 10 zombie-villageois persistants** dans
104 chunks FULL. Entrée témoin : `-16, 112, 19`. Les 99 pièces natives et la
cavité conservent leurs données à la sérialisation ; 29 pièces subissent la
normalisation verticale native décrite plus haut. Une mine est réservée.
Les tests contrôlent également le marais, les rivières, la traversée,
les satellites et la localisation native.
Le plan de surface contient 11 parcours et 52 segments : **659 colonnes de
voie**, 1 982 colonnes aménagées en incluant les chemins. Cela représente
**0,90 %** de lemprise terrestre projetée estimée (0,96 % au-dessus de Y180),
et non les 68 % évoqués comme repère visuel. Le réseau est plus ramifié mais
ne couvre pas toute l’île. Les coupes du village montrent les blocs observés
après décoration ; les tracés ferroviaires représentent le plan préparé.
## Plans finaux Grand / graine utilisateur
Une nouvelle préparation native, après la dernière sculpture aérienne, passe
**2/2 tests en 3 min 1 s**, démarrage compris. Le village (12 maisons, entrée
`-59, 112, 336`) et les deux emprises de mines sont identiques au témoin
complet précédent. Le plan de surface final possède 11 parcours, 66 segments,
844 colonnes de voie et 2 538 colonnes aménagées, soit **0,48 %** de la surface
terrestre projetée estimée. Ce dernier essai vérifie les plans et leur
sérialisation, sans prétendre rejouer toute la génération FULL du Grand.
## Livraison et limites
Lassemblage utilise `./gradlew assemblePack -x :sanctuary:check` après le
`check build` complet réussi, pour ne pas relancer une seconde fois tous les
mêmes tests par la dépendance `assemblePack → build → check`. Le premier
assemblage sans exclusion a été interrompu avant cette répétition native.
Lassemblage réussit ; JAR, index packwiz, MRpack, dépendance Fabric API et
amorçage Prism sont vérifiés localement avant publication.
Le reçu de publication et de synchronisation figure dans `docs/packwiz.md`.
Larchive fige les mesures, les images et lempreinte du JAR de cette livraison.
Les pourcentages de voies sont des mesures du plan ; aucun trajet en minecart
sur lintégralité du réseau nest revendiqué. Les recherches bornées peuvent
refuser une autre graine ; les témoins testés ne prouvent pas toutes les graines.
Aucune arène ni plateforme à Y600 nest implémentée.
+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.30.3
pack_version=0.1.0-alpha.30.3
mod_version=0.1.0-alpha.30.6
pack_version=0.1.0-alpha.30.6
maven_group=fr.koka.sanctuary
+13 -1
View File
@@ -24,6 +24,9 @@ if (clientRenderTests) {
sourceSets.named('gametest') {
java.srcDir('src/clientGametest/java')
}
tasks.named('runClientGameTest') {
systemProperty('sanctuary.test.highland304', providers.gradleProperty('sanctuaryHighland304Visual').getOrElse('false'))
}
}
// Fresh generation by default; the explicit laboratory reload verifies the same saved test world.
@@ -33,6 +36,10 @@ tasks.register('cleanGameTestWorld', Delete) {
}
tasks.named('runGameTest') {
dependsOn('cleanGameTestWorld')
systemProperty('sanctuary.test.village306.planOnly', providers.gradleProperty('sanctuaryVillagePlanOnly').getOrElse('false'))
systemProperty('sanctuary.test.atlasOnly', providers.gradleProperty('sanctuaryAtlasOnly').getOrElse('false'))
systemProperty('sanctuary.test.atlasSeeds', providers.gradleProperty('sanctuaryAtlasSeeds').getOrElse('0,42,-7228211907433324401'))
systemProperty('sanctuary.test.atlasRevision', providers.gradleProperty('sanctuaryAtlasRevision').getOrElse('working-tree'))
systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
@@ -81,6 +88,10 @@ tasks.named('runGameTest') {
}
tasks.named('processGametestResources') {
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
inputs.property('villageOnly', villageOnly)
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
inputs.property('atlasOnly', atlasOnly)
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
inputs.property('benchmark', benchmark)
def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true)
@@ -124,10 +135,11 @@ tasks.named('processGametestResources') {
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('voidReuse', voidReuse)
filesMatching('fabric.mod.json') {
expand(gametest_entrypoint: aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
expand(gametest_entrypoint: villageOnly ? 'fr.koka.sanctuary.gametest.Village306GameTests' : atlasOnly ? 'fr.koka.sanctuary.gametest.TerrainAtlas305GameTests' : aerial24Flow ? 'fr.koka.sanctuary.gametest.Aerial24FlowAdmissionGameTests' : aerial24 ? 'fr.koka.sanctuary.gametest.Aerial24WorldGameTests' : admission23 ? 'fr.koka.sanctuary.gametest.Expansion23AdmissionGameTests' : ocean23 ? 'fr.koka.sanctuary.gametest.Ocean23WorldGameTests' : secrets23 ? 'fr.koka.sanctuary.gametest.Secrets23WorldGameTests' : native23 ? 'fr.koka.sanctuary.gametest.Native23WorldGameTests' : selective22 ? 'fr.koka.sanctuary.gametest.Selective22WorldGameTests' : transit21 ? 'fr.koka.sanctuary.gametest.Transit21WorldGameTests' : lostCity21 ? 'fr.koka.sanctuary.gametest.LostCity21WorldGameTests' : heritage21 ? 'fr.koka.sanctuary.gametest.Heritage21WorldGameTests' : lostCity20 ? 'fr.koka.sanctuary.gametest.LostCity20WorldGameTests' : heritage20 ? 'fr.koka.sanctuary.gametest.Heritage20WorldGameTests' : heritage19 ? 'fr.koka.sanctuary.gametest.Heritage19WorldGameTests' : ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
if (atlasOnly || villageOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
}
}
@@ -0,0 +1,62 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.client.SanctuarySky;
import java.nio.file.Path;
import java.util.UUID;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.chunk.status.ChunkStatus;
/** Opt-in photographs of actual decorated terrain, in a fresh ignored development world only. */
final class HighPlateaus304ClientChecks {
static void run(ClientGameTestContext context) {
Path root = Path.of("").toAbsolutePath().normalize();
if (!root.endsWith(Path.of("build", "run", "clientGameTest")))
throw new AssertionError("Visual test must stay in the ignored client development directory");
try (var world = context.worldBuilder().adjustSettings(settings -> {
settings.setName("Sanctuary relief304 " + UUID.randomUUID());
settings.setSeed("0");
var preset = settings.getSettings().worldgenLoadContext().lookupOrThrow(Registries.WORLD_PRESET)
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary")));
settings.setWorldType(new WorldCreationUiState.WorldTypeEntry(preset));
settings.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
settings.setGenerateStructures(false);
}).create()) {
context.runOnClient(client -> {
client.options.renderDistance().set(8);
client.options.broadcastOptions();
});
world.getServer().runOnServer(server -> {
server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), "gamemode spectator @a");
server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), "time set 6000");
server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), "tp @a 150 288 -60 0 -20");
});
context.waitFor(client -> client.player.isSpectator() && Math.abs(client.player.getY() - 288) < 1, 2400);
context.waitTicks(40);
// ClientLevel.hasChunk() always returns true; require actual received chunks instead.
context.waitFor(client -> client.level.getChunk(9, 0, ChunkStatus.FULL, false) != null
&& client.level.getChunk(11, 3, ChunkStatus.FULL, false) != null, 2400);
world.getConnection().waitForChunksRender(false, 2400);
context.waitTicks(20);
context.runOnClient(client -> {
if (client.level.getHeight() != 640 || !SanctuarySky.isFloatingWorld(client.level))
throw new AssertionError("The connected client needs tall bounds and Sanctuary sky");
});
context.takeScreenshot("sanctuary-alpha304-cliff");
world.getServer().runOnServer(server -> server.getCommands().performPrefixedCommand(
server.createCommandSourceStack(), "tp @a -45 407 30 0 40"));
context.waitFor(client -> Math.abs(client.player.getY() - 407) < 1, 2400);
context.waitTicks(40);
context.waitFor(client -> client.level.getChunk(-6, 8, ChunkStatus.FULL, false) != null
&& client.level.getChunk(-1, 9, ChunkStatus.FULL, false) != null, 2400);
world.getConnection().waitForChunksRender(false, 2400);
context.waitTicks(20);
context.takeScreenshot("sanctuary-alpha304-high-river");
} finally {
context.restoreDefaultGameOptions();
}
}
}
@@ -5,7 +5,7 @@ import java.util.Arrays;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
/** Opt-in check of production render mixins in the actual client, without opening a save. */
/** Opt-in render, UI and network checks in fresh ignored development worlds. */
public final class SanctuaryClientRenderTests implements FabricClientGameTest {
@Override
public void runTest(ClientGameTestContext context) {
@@ -17,6 +17,7 @@ public final class SanctuaryClientRenderTests implements FabricClientGameTest {
SanctuaryMod.LOGGER.info("Client render tests passed: Sanctuary sky and fog mixins applied.");
});
SanctuaryWorldOptionsClientChecks.run(context);
if (Boolean.getBoolean("sanctuary.test.highland304")) HighPlateaus304ClientChecks.run(context);
BlocodexClientChecks.run(context);
BlocodexNetworkClientChecks.run(context);
}
@@ -89,8 +89,15 @@ final class SanctuaryWorldOptionsClientChecks {
require(client.gui.screen() == create && diameter(create) == size.getValue(),
"Done must apply the chosen diameter and return to creation");
var after = create.getUiState().getSettings();
var type = after.selectedDimensions().get(LevelStem.OVERWORLD).orElseThrow().type();
require(type.value().height() == 640 && type.value().minY() == 0,
"Every customized size keeps the 640-block dimension");
require(fr.koka.sanctuary.client.SanctuarySky.isFloatingType(type),
"The tall dimension uses the Sanctuary sky and open-void fog");
require(after.options().equals(before.options()), "Size must preserve seed, structures and bonus chest");
for (var dimension : List.of(LevelStem.NETHER, LevelStem.END)) {
require(!fr.koka.sanctuary.client.SanctuarySky.isFloatingType(
after.selectedDimensions().get(dimension).orElseThrow().type()), "Native sky remains native outside Sanctuary");
require(after.selectedDimensions().get(dimension).equals(before.selectedDimensions().get(dimension)),
"Size must preserve other dimensions");
}
@@ -149,6 +149,23 @@ public final class Aerial24WorldGameTests {
report.put("mutations", previous.get("mutations"));
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
}
helper.assertTrue(level.getMinY() == 0 && level.getHeight() == 640
&& generator.getGenDepth() == 640, "Dimension and generator expose Y0..639");
helper.assertTrue(generator.starterContext().generator().getGenDepth() == 640,
"The native terrain delegate also exposes the extended build height");
var ceilingWitness = new BlockPos(8, 639, 8);
if (create) {
for (int y = 384; y < 640; y++)
helper.assertTrue(level.getBlockState(new BlockPos(8,y,8)).isAir(),
"The new build space does not lift the entire island");
helper.assertTrue(level.setBlock(ceilingWitness, Blocks.DIAMOND_BLOCK.defaultBlockState(), 3),
"A normal block can be built at Y639");
helper.assertFalse(level.setBlock(ceilingWitness.above(), Blocks.DIAMOND_BLOCK.defaultBlockState(), 3),
"Y640 is outside the build boundary");
}
helper.assertTrue(level.getBlockState(ceilingWitness).is(Blocks.DIAMOND_BLOCK),
"A block built at Y639 survives saving and a separate JVM reload");
report.put("build_height", 640); report.put("ceiling_block", List.of(8,639,8));
var tags = tags(level, starts);
if (!create) helper.assertTrue(previous.get("native_tags").equals(JSON.toJsonTree(tags)), "Native starts, references and occupant flags survive both reloads");
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
@@ -70,11 +70,14 @@ public final class Alpha28NativeStructuresGameTests {
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)
var searchChunks = new java.util.LinkedHashSet<ChunkPos>();
ExpansionStructures23.candidates(root).forEach(c -> searchChunks.add(c.chunk()));
host.starterMines306().forEach(m -> searchChunks.add(m.start().getChunkPos()));
for (var candidate : searchChunks) {
var start = level.getChunk(candidate.x(), candidate.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;
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.x(), candidate.z())); break;
}
}
helper.assertTrue(found.containsKey(id), "The native /locate result belongs to a real saved start");
@@ -27,6 +27,9 @@ public final class River30WorldGameTests {
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));
for(var feature:plan.features()) if(feature.kind()==PopulationHydrology.Kind.RIVER)
selected.stream().filter(c->c.featureId()==feature.id()).findFirst().ifPresent(witnesses::add);
report.put("tested_water_levels",witnesses.stream().map(PopulationHydrology.Cell::waterY).distinct().sorted().toList());
var falls=plan.spills().stream().filter(f -> plan.features().stream().anyMatch(feature -> feature.kind()==PopulationHydrology.Kind.OUTLET && feature.id()==f.id())).limit(2).toList();
if(Boolean.getBoolean("sanctuary.test.requireCurvedRiverBank"))
helper.assertTrue(falls.stream().anyMatch(f -> plan.features().stream().anyMatch(feature -> feature.id()==f.fromFeatureId() && feature.kind()==PopulationHydrology.Kind.RIVER)),
@@ -121,8 +121,10 @@ public final class SanctuaryWorldGameTests {
var level = helper.getLevel();
var registries = level.registryAccess();
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS);
helper.assertTrue(level.getMinY() == 0 && level.getHeight() == 384,
"The new Sanctuary dimension must have exactly 384 buildable blocks, Y0..383");
boolean tall = level.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator sg
&& sg.rootGeneration() == 24;
helper.assertTrue(level.getMinY() == 0 && level.getHeight() == (tall ? 640 : 384),
"Current Sanctuary has 640 buildable blocks; historical presets retain 384");
float cloudHeight = level.environmentAttributes()
.getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.CLOUD_HEIGHT);
helper.assertTrue(Math.abs(cloudHeight - 352.33F) < 0.001F,
@@ -155,7 +155,7 @@ public final class Swamp27WorldGameTests {
if (state.getFluidState().is(FluidTags.WATER)) { water++; chunkWater++; }
if (plant(state)) {
// A feature must obey native placement; old vanilla vines elsewhere are irrelevant.
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state);
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state + " below=" + level.getBlockState(at.below()));
plants++; chunkPlants++; counts.merge(id(state), 1, Integer::sum);
}
if ((!state.isAir() && !plant(state)) || !state.getFluidState().isEmpty()) continue;
@@ -16,13 +16,13 @@ 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 broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception {
public void broadProvincesPreservePlateausWithoutStretchingCaves(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.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.3 removes fine bands and uses broad geological provinces");
helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.5 keeps broad plateaus and a moderate native 3D field");
var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
var context = SamplerContext.builder().enableCaches().build();
@@ -43,7 +43,7 @@ public final class Terraces29WorldGameTests {
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
}
}
helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and raised rock");
helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and native 3D rock");
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>();
@@ -108,7 +108,7 @@ public final class Terraces29WorldGameTests {
var columns = new ArrayList<Object>(); int retained=0,land=0,highest=0;
for(int z=-r;z<=r;z+=12)for(int x=-r;x<=r;x+=12){
int a=0,b=0,c=0;
for(int y=380;y>=32;y-=2){
for(int y=398;y>=32;y-=2){
if(a==0 && old.sampleValue(context,x,y,z)>0)a=y;
if(b==0 && now.sampleValue(context,x,y,z)>0)b=y;
if(c==0 && global.sampleValue(context,x,y,z)>0)c=y;
@@ -117,13 +117,13 @@ public final class Terraces29WorldGameTests {
if(a>=200){land++;if(Math.abs(a-b)<=2)retained++;}
highest=Math.max(highest,b); columns.add(List.of(x,z,a,b,c));
}
helper.assertTrue(highest<=358,"Natural summits leave room below the build ceiling");
helper.assertTrue(highest<400,"Natural terrain and upper bodies leave at least 240 blocks below the build ceiling");
helper.assertTrue(retained>land*.25,"Each size retains substantial original uplands, rather than changing every slope");
sizes.add(Map.of("diameter",r*2,"columns",columns,"unchanged_uplands",retained,"uplands",land,"highest_sample",highest));
}
report.put("sizes",sizes);
report.put("passed", true);
Path path = Path.of("diagnostics/alpha303", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Path path = Path.of("diagnostics/alpha305", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
@@ -0,0 +1,136 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.Gson;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Scientific export of native registered fields, before fluids, surfaces and structures.
* Flat test bootstrap only: no Sanctuary chunks or expansion reservations are needed. */
public final class TerrainAtlas305GameTests {
@GameTest(maxTicks = 120000)
public void exportNativeAtlas(GameTestHelper helper) throws Exception {
var registries = helper.getLevel().registryAccess();
String version = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
.getMetadata().getVersion().getFriendlyString();
for (String seedText : System.getProperty("sanctuary.test.atlasSeeds", "0").split(",")) {
long seed = Long.parseLong(seedText.trim());
for (int players : new int[]{5, 10, 20}) {
long started = System.nanoTime();
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_" + players))).value();
var random = RandomState.create(registries.lookupOrThrow(Registries.NOISE), seed, settings);
var field = (PopulationIslandDensity) registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + players))).value();
int radius = (int)Math.round(field.capacity().radius());
int bound = ((radius + 32 + 11) / 12) * 12;
var functions = new LinkedHashMap<String, DensityFunction>();
functions.put("plateaus27", field.legacy27());
functions.put("global301", field.legacy301());
functions.put("current305", new Historical305(field));
functions.put("current306", field);
var variants = new LinkedHashMap<String, Object>();
for (var entry : functions.entrySet()) {
var sampler = random.getSampler(entry.getValue());
var context = SamplerContext.builder().enableCaches().build();
var columns = new ArrayList<List<Integer>>();
for (int z = -bound; z <= bound; z += 12) for (int x = -bound; x <= bound; x += 12) {
int top = 0, rock = 0, skin = 0;
boolean roof = false;
for (int y = 418; y >= 2; y -= 2) {
boolean solid = sampler.sampleValue(context, x, y, z) > 0;
if (solid) {
rock += 2;
if (top == 0) { top = y; roof = true; }
if (roof) skin += 2;
} else if (top != 0) roof = false;
}
columns.add(List.of(x, z, top, rock, skin));
}
var sections = new ArrayList<Object>();
for (int z : new int[]{0, 96, -(radius / 4)}) {
var rows = new ArrayList<Object>();
for (int y = 418; y >= 0; y -= 2) {
var row = new ArrayList<Integer>();
for (int x = -bound; x <= bound; x += 2) {
float density = sampler.sampleValue(context, x, y, z);
helper.assertTrue(Float.isFinite(density), "Native density is finite");
// Quantization preserves the sign around the zero isosurface.
int encoded = Math.round(Math.clamp(density, -1, 1) * 10000);
if (density > 0 && encoded == 0) encoded = 1;
if (density < 0 && encoded == 0) encoded = -1;
row.add(encoded);
}
rows.add(row);
}
sections.add(Map.of("z", z, "min_x", -bound, "max_y", 418, "step", 2, "density", rows));
}
var upperCuts = new ArrayList<Object>();
if(entry.getKey().equals("current306")) {
var peaks=new ArrayList<List<Integer>>();
for(var peak:columns.stream().filter(c->c.get(2)>=300).sorted(Comparator.comparingInt((List<Integer> c)->c.get(2)).reversed()).toList()) {
if(peaks.stream().anyMatch(p->Math.hypot(p.get(0)-peak.get(0),p.get(1)-peak.get(1))<100))continue;
peaks.add(peak); if(peaks.size()==3)break;
}
helper.assertTrue(peaks.size()>=2,"At least two substantial upper bodies sampled");
for(var peak:peaks) {
var rows=new ArrayList<Object>();
for(int y=418;y>=260;y-=2) {var row=new ArrayList<Integer>();for(int x=peak.get(0)-72;x<=peak.get(0)+72;x+=2)
row.add(sampler.sampleValue(context,x,y,peak.get(1))>0?1:0);rows.add(row);}
upperCuts.add(Map.of("z",peak.get(1),"min_x",peak.get(0)-72,"max_y",418,"step",2,"solid",rows));
}
}
variants.put(entry.getKey(), Map.of("columns", columns, "sections", sections,"upper_sections",upperCuts));
}
var report = new LinkedHashMap<String, Object>();
report.put("schema", 1); report.put("version", version);
report.put("revision", System.getProperty("sanctuary.test.atlasRevision", "working-tree"));
report.put("minecraft", "26.3-pre-2"); report.put("seed", Long.toString(seed));
report.put("diameter", radius * 2); report.put("process_id", ProcessHandle.current().pid());
report.put("domain_y", List.of(0, 419)); report.put("build_ceiling_exclusive", 640);
report.put("map_step", 12); report.put("vertical_step", 2); report.put("density_scale", 10000);
report.put("columns", List.of("x", "z", "top_y", "sampled_solid_thickness", "uninterrupted_roof_thickness"));
report.put("scope", "Native density, before fluids, surface materials, vegetation and structures; no chunks generated for this export");
report.put("variants", variants);
report.put("export_seconds", (System.nanoTime() - started) / 1e9);
Path path = Path.of("diagnostics/atlas306", "terrain-" + radius * 2 + "-" + seed + ".json");
Files.createDirectories(path.getParent()); Files.writeString(path, new Gson().toJson(report));
SanctuaryMod.LOGGER.info("Terrain atlas exported {} in {} s", path, report.get("export_seconds"));
}
}
helper.succeed();
}
private record Historical304(PopulationIslandDensity base) implements DensityFunction {
@Override public DensitySampler compileSampler(CompileContext context) { return base.compileHistorical304(context); }
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new Historical304((PopulationIslandDensity)rule.rewrite(base));
}
@Override public Interval range() { return base.range(); }
@Override public int domainAxes() { return ALL_AXES; }
// Never serialized: this wrapper only selects the exact diagnostic sampler.
@Override public MapCodec<? extends DensityFunction> codec() { return base.codec(); }
}
private record Historical305(PopulationIslandDensity base) implements DensityFunction {
@Override public DensitySampler compileSampler(CompileContext context) { return base.compileHistorical305(context); }
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new Historical305((PopulationIslandDensity)rule.rewrite(base));
}
@Override public Interval range() { return base.range(); }
@Override public int domainAxes() { return ALL_AXES; }
// Never serialized: this wrapper only selects the exact diagnostic sampler.
@Override public MapCodec<? extends DensityFunction> codec() { return base.codec(); }
}
}
@@ -0,0 +1,95 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.underground306.Village306;
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.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
public final class Village306GameTests {
@GameTest(maxTicks=12000)
public void villageInCustomCavern(GameTestHelper helper) throws Exception {
var level=helper.getLevel();var host=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();var p=host.village306();
if(host.rootGeneration()!=24){helper.succeed();return;}
var report=new LinkedHashMap<String,Object>();report.put("seed",Long.toString(level.getSeed()));report.put("diameter",host.initialDiameter());
report.put("process_id",ProcessHandle.current().pid());report.put("diagnostics",p.diagnostics());
Path file=Path.of("diagnostics/village306",host.initialDiameter()+"-"+level.getSeed()+".json");Files.createDirectories(file.getParent());
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
if(!host.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {helper.assertTrue(p.start()==null,"Options disable village");helper.succeed();return;}
helper.assertTrue(p.start()!=null,"Underground vanilla village admitted: "+p.diagnostics());
helper.assertTrue(host.starterMines306().size()>=(host.initialDiameter()==1024?2:1),"One native mine, two for Large");
report.put("mines",host.starterMines306().stream().map(m->m.start().getBoundingBox().toString()).toList());
report.put("rail_links",host.heritagePlan19().links().stream().map(l->Map.of("id",l.id(),"kind",l.kind().toString(),"points",l.path())).toList());
report.put("rail_cells",host.heritagePlan19().segments().stream().flatMap(seg->seg.cells().stream()).toList());
report.put("box",p.start().getBoundingBox().toString());report.put("floor",p.cavern().floorY());report.put("entrance",p.cavern().getLocatorPosition().toShortString());
var cp=p.start().getChunkPos();var actual=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(p.start().getStructure());
helper.assertTrue(actual!=null&&actual.isValid(),"Actual registered start");
var ctx=StructurePieceSerializationContext.fromLevel(level);var tag=actual.createTag(ctx,cp);var restored=StructureStart.loadStaticStart(ctx,tag,level.getSeed());
helper.assertTrue(restored!=null&&restored.getPieces().size()==actual.getPieces().size(),"Full graph reloads without losing pieces");
var restoredTag=restored.createTag(ctx,cp);
report.put("exact_nbt_roundtrip",tag.equals(restoredTag));
int normalized=0;
for(int i=0;i<actual.getPieces().size();i++) {
var before=actual.getPieces().get(i); var after=restored.getPieces().get(i);
var a=before.createTag(ctx); var b=after.createTag(ctx);
if(before instanceof net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece) {
// Native jigsaw collision padding is discarded on template reload. Placement,
// projection, junctions, rotation and the template itself must remain identical.
var x=before.getBoundingBox();var y=after.getBoundingBox();
helper.assertTrue(x.minX()==y.minX()&&x.maxX()==y.maxX()&&x.minZ()==y.minZ()&&x.maxZ()==y.maxZ()
&&x.minY()==y.minY()&&y.maxY()<=x.maxY(),"Native reload only removes vertical collision padding");
if(!x.equals(y))normalized++;
a.remove("BB");b.remove("BB");
}
helper.assertTrue(a.equals(b),"Every piece preserves its placement and contents through NBT");
}
report.put("native_padding_normalized",normalized);
if(Boolean.getBoolean("sanctuary.test.village306.planOnly")) {
report.put("passed",true);Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();return;
}
var chunks=p.start().getBoundingBox().intersectingChunks().toList();var iterator=chunks.iterator();Runnable[] next=new Runnable[1];
boolean[] tickets={false}; int[] waited={0};
next[0]=()->{try{
if(iterator.hasNext()){var at=iterator.next();level.getChunk(at.x(),at.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->next[0].run());return;}
if(!tickets[0]) {chunks.forEach(at->level.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,at,2));tickets[0]=true;}
if((waited[0] < 20 || level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.ZombieVillager.class,
net.minecraft.world.phys.AABB.of(p.cavern().getBoundingBox())).isEmpty()) && waited[0]++<200) {
helper.runAtTickTime(helper.getTick()+1,()->next[0].run());return;
}
int beds=0,chests=0,roads=0; var box=p.cavern().getBoundingBox();
for(int z=box.minZ();z<=box.maxZ();z++)for(int x=box.minX();x<=box.maxX();x++)for(int y=p.cavern().floorY()-2;y<=p.cavern().floorY()+13;y++){
var state=level.getBlockState(new BlockPos(x,y,z));if(state.getBlock() instanceof net.minecraft.world.level.block.BedBlock)beds++;
if(state.is(net.minecraft.world.level.block.Blocks.CHEST))chests++;if(state.is(net.minecraft.world.level.block.Blocks.DIRT_PATH))roads++;
}
var zombies=level.getEntitiesOfClass(net.minecraft.world.entity.monster.zombie.ZombieVillager.class,net.minecraft.world.phys.AABB.of(box));
report.put("beds_blocks",beds);report.put("chests",chests);report.put("roads",roads);report.put("zombie_villagers",zombies.size());report.put("entity_wait",waited[0]);
report.put("monster_rooms",fr.koka.sanctuary.knowledge.structures.StructureCensusService.snapshot(level.getServer()).evidence().sites().values().stream()
.filter(site->site.key().id().equals("minecraft:monster_room")).map(site->site.key()).toList());report.put("difficulty",level.getDifficulty().toString());
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.assertTrue(zombies.stream().allMatch(z->z.isPersistenceRequired()),"Native zombie villagers remain persistent and curable");
helper.assertTrue(beds>=6,"Vanilla houses have beds");helper.assertTrue(chests>0,"Native chest loot present");helper.assertTrue(roads>20,"Native roads placed underground");helper.assertTrue(!zombies.isEmpty(),"Abandoned village has zombie villagers");
report.put("beds_blocks",beds);report.put("chests",chests);report.put("roads",roads);report.put("zombie_villagers",zombies.size());report.put("full_chunks",chunks.size());report.put("passed",true);
var cuts=new ArrayList<Object>();
for(int z:new int[]{box.minZ()+box.getZSpan()/3,box.minZ()+box.getZSpan()*2/3}) {
var rows=new ArrayList<List<Integer>>();
for(int y=300;y>=0;y-=2){var row=new ArrayList<Integer>();for(int x=box.minX();x<=box.maxX();x+=2){
var state=level.getBlockState(new BlockPos(x,y,z));row.add(state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)?2:
state.getFluidState().is(net.minecraft.tags.FluidTags.LAVA)?3:state.isAir()?0:1);
}rows.add(row);}cuts.add(Map.of("z",z,"min_x",box.minX(),"max_y",300,"step",2,"blocks",rows));
}
report.put("native_cuts",cuts);
var floorRows=new ArrayList<List<String>>();
for(int z=box.minZ();z<=box.maxZ();z++){var row=new ArrayList<String>();for(int x=box.minX();x<=box.maxX();x++)
row.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(level.getBlockState(new BlockPos(x,p.cavern().floorY()+2,z)).getBlock()).toString());floorRows.add(row);}
report.put("floor_cut",Map.of("min_x",box.minX(),"min_z",box.minZ(),"y",p.cavern().floorY()+2,"blocks",floorRows));
Files.writeString(file,new GsonBuilder().setPrettyPrinting().create().toJson(report));
chunks.forEach(at->level.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.DRAGON,at,2));helper.succeed();
}catch(Exception error){throw new IllegalStateException(error);}};
helper.runAtTickTime(helper.getTick()+1,()->next[0].run());
}
}
@@ -46,6 +46,7 @@ public abstract class SanctuaryGameTestServerMixin {
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
if (Boolean.getBoolean("sanctuary.test.atlasOnly")) return net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT_ALL_DIMENSIONS;
if (Boolean.getBoolean("sanctuary.test.unified")) {
int generation = Integer.getInteger("sanctuary.test.generation", 24);
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17 && generation != 19 && generation != 20 && generation != 21 && generation != 22 && generation != 23 && generation != 24) throw new IllegalArgumentException("Unsupported test generation");
@@ -68,7 +69,7 @@ public abstract class SanctuaryGameTestServerMixin {
private static net.minecraft.world.level.levelgen.WorldDimensions sanctuary$customizeSize(WorldPreset preset,
net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) {
var dimensions = preset.createWorldDimensions();
if (Boolean.getBoolean("sanctuary.test.unified")) {
if (Boolean.getBoolean("sanctuary.test.unified") && !Boolean.getBoolean("sanctuary.test.atlasOnly")) {
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
@@ -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.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-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.Village306GameTests", "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"],
@@ -79,6 +79,7 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v24"), SanctuaryChunkGenerator.AERIAL24_CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v24"), SanctuaryBiomeSource.AERIAL24_CODEC);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.register();
fr.koka.sanctuary.worldgen.underground306.Village306.register();
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.register();
fr.koka.sanctuary.worldgen.transit.TransitStructures21.register();
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.register();
@@ -7,11 +7,16 @@ import net.minecraft.resources.Identifier;
/** Client visuals belonging only to the new floating-world dimension type. */
public final class SanctuarySky {
private static final Identifier FLOATING_WORLD = SanctuaryMod.id("sanctuary_384");
private static final Identifier TALL_FLOATING_WORLD = SanctuaryMod.id("sanctuary_640");
private SanctuarySky() {
}
public static boolean isFloatingWorld(ClientLevel level) {
return level.dimensionTypeRegistration().is(FLOATING_WORLD);
return isFloatingType(level.dimensionTypeRegistration());
}
public static boolean isFloatingType(net.minecraft.core.Holder<net.minecraft.world.level.dimension.DimensionType> type) {
return type.is(FLOATING_WORLD) || type.is(TALL_FLOATING_WORLD);
}
}
@@ -130,6 +130,14 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
fr.koka.sanctuary.worldgen.city.LostCityUndergroundStructures22.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.generate(this, registries, state, structures, chunk, templates, dimension);
fr.koka.sanctuary.worldgen.aerial.AerialStructures24.generate(this, registries, state, structures, chunk, templates, dimension);
if (this instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator host && structures.shouldGenerateStructures()
&& Level.OVERWORLD.equals(dimension) && host.village306().start() != null) {
var planned = host.village306().start();
if (planned.getChunkPos().equals(chunk.getPos())) {
var existing = structures.getStartForStructure(planned.getStructure(), chunk);
if (existing == null || !existing.isValid()) structures.setStartForStructure(planned.getStructure(), planned, chunk);
}
}
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
}
@Override public void spawnOriginalMobs(WorldGenRegion region) {
@@ -187,7 +195,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
}
return column;
}
BlockState[] air = new BlockState[384];
BlockState[] air = new BlockState[getGenDepth()];
java.util.Arrays.fill(air, Blocks.AIR.defaultBlockState());
return new NoiseColumn(0, air);
}
@@ -92,6 +92,13 @@ public final class ExpansionStructures23 {
if (!Level.OVERWORLD.equals(dimension) || !manager.shouldGenerateStructures()) return;
ExpansionIsland island = generator.islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
if (island == null || !nativeGeneration(island.generation())) return;
if (island.origin() && generator instanceof SanctuaryChunkGenerator host) {
for (var planned : host.starterMines306()) if (planned.start().getChunkPos().equals(chunk.getPos())) {
var existing = manager.getStartForStructure(planned.structure().value(),chunk);
if(existing == null || !existing.isValid()) manager.setStartForStructure(planned.structure().value(),planned.start(),chunk);
return;
}
}
Candidate candidate = candidates(island).stream().filter(c -> c.chunk().equals(chunk.getPos())).findFirst().orElse(null);
if (candidate == null) return;
for (var old : chunk.getAllStarts().values()) {
@@ -159,6 +166,37 @@ public final class ExpansionStructures23 {
return null;
}
/** Reserve complete native mines for the starter island, before mutable chunk visitation. */
public static List<Accepted> starterMines306(ServerLevel level, SanctuaryChunkGenerator host, ExpansionIsland root) {
var view = new NativeView(host, root, host.source(), level.registryAccess());
var factory = PalettedContainerFactory.create(level.registryAccess());
var choices = new ArrayList<>(candidates(root));
choices.sort(Comparator.comparingDouble(c -> Math.hypot(c.chunk().getMiddleBlockX(), c.chunk().getMiddleBlockZ())));
// Additional initial-world candidates vary the complete native graph, rather than forcing
// one unsuitable graph through the island. They become real locator candidates when reserved.
var extra = new ArrayList<Candidate>();
int extent = root.terrainRadius() * 3 / 4;
for(int z=-extent;z<=extent;z+=48)for(int x=-extent;x<=extent;x+=48) {
var at=new ChunkPos(x>>4,z>>4);
if(Math.hypot(x,z)>extent || choices.stream().anyMatch(c->c.chunk().equals(at)))continue;
extra.add(new Candidate(at,mix(root.seed()^at.pack()^0x306A11EL)));
}
extra.sort(Comparator.comparingLong(Candidate::roll));choices.addAll(extra);
var result = new ArrayList<Accepted>();
for (var candidate : choices.stream().limit(192).toList()) {
var chunk = new ProtoChunk(candidate.chunk(), UpgradeData.EMPTY, level, factory, null);
try {
var accepted = planFiltered(host, view, root, candidate, level.registryAccess(), level.getStructureTemplateManager(),
level.dimension(), chunk, Long.MAX_VALUE, entry -> entry.id().equals("minecraft:mineshaft"));
if (accepted == null || result.stream().anyMatch(old -> old.start().getBoundingBox().intersects(accepted.start().getBoundingBox()))) continue;
result.add(accepted);
if (result.size() >= (host.initialDiameter() == 1024 ? 2 : 1)) break;
} catch (SampleBudgetExceeded refused) { /* Continue the finite search, retaining native support rules. */ }
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("Starter mines306 seed {} size {}: {} native mines reserved", root.seed(),host.initialDiameter(),result.size());
return List.copyOf(result);
}
private static Accepted planFiltered(ExpansionChunkGenerator host, ExpansionChunkGenerator view, ExpansionIsland island,
Candidate candidate, RegistryAccess registries, StructureTemplateManager templates, ResourceKey<Level> dimension,
ChunkAccess chunk, long deadlineNanos, Predicate<Entry> included) {
@@ -469,6 +507,11 @@ public final class ExpansionStructures23 {
&& Math.abs((long) candidate.chunk().z() - (origin.getZ() >> 4)) <= (long) radius * CELL_CHUNKS) candidates.add(candidate);
}
}
if (nativeRequested && generator instanceof SanctuaryChunkGenerator sanctuary) {
for(var mine:sanctuary.starterMines306()) {
var at=mine.start().getChunkPos();addPosition(candidates,at.x(),at.z());
}
}
if (sanctuaryRequested && generator instanceof SanctuaryChunkGenerator sanctuary) addSanctuaryCandidates(sanctuary, candidates);
candidates.removeIf(c -> Math.abs((long) c.chunk().x() - (origin.getX() >> 4)) > (long) radius * CELL_CHUNKS
|| Math.abs((long) c.chunk().z() - (origin.getZ() >> 4)) > (long) radius * CELL_CHUNKS);
@@ -508,6 +551,9 @@ public final class ExpansionStructures23 {
generator.ruinsPlan17().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
generator.transitPlan21().modules().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
generator.undergroundHall22().networks().forEach(s -> addPosition(positions, s.startChunkX(), s.startChunkZ()));
if (generator.village306().start() != null) {
var village = generator.village306().start().getChunkPos(); addPosition(positions, village.x(), village.z());
}
var mineral = generator.mineralSanctuary23();
if (!mineral.isEmpty()) addPosition(positions, mineral.chamber().startChunkX(), mineral.chamber().startChunkZ());
if (generator.rootGeneration() == 24) generator.aerialPlan24().sites()
@@ -214,11 +214,19 @@ public final class StructureCensusService {
var session = current(server); if (session.failed) return;
try { observation.accept(session); } catch (RuntimeException error) { fail(session, error); }
};
if (server.isSameThread()) task.run(); else server.execute(task);
if (server.isSameThread()) task.run(); else if (server.isRunning()) server.execute(() -> {
// Minecraft may execute inline on a generation worker during shutdown. Such late
// chunks stay unknown in the census and are observed normally on their next load.
if (server.isSameThread()) task.run();
});
}
private static void reportFailure(MinecraftServer server, Exception error) {
Runnable task = () -> fail(current(server), error);
if (server.isSameThread()) task.run(); else server.execute(task);
if (server.isSameThread()) task.run(); else if (server.isRunning()) server.execute(() -> {
// Minecraft may execute inline on a generation worker during shutdown. Such late
// chunks stay unknown in the census and are observed normally on their next load.
if (server.isSameThread()) task.run();
});
}
public static void flush(MinecraftServer server) {
assertServerThread(server); var session = SERVERS.get(server);
@@ -0,0 +1,20 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.underground306.Village306;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.levelgen.Heightmap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Vanilla terrain_matching roads query this at placement time, independently of the planning generator. */
@Mixin(WorldGenRegion.class)
public abstract class UndergroundHeight306Mixin {
@Inject(method = "getHeight", at = @At("HEAD"), cancellable = true)
private void sanctuary$undergroundRoad306(Heightmap.Types type, int x, int z, CallbackInfoReturnable<Integer> cir) {
Integer floor = Village306.projectedHeight(x, z);
if (floor != null && (type == Heightmap.Types.WORLD_SURFACE_WG || type == Heightmap.Types.OCEAN_FLOOR_WG
|| type == Heightmap.Types.WORLD_SURFACE || type == Heightmap.Types.OCEAN_FLOOR)) cir.setReturnValue(floor);
}
}
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.mixin;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import fr.koka.sanctuary.worldgen.underground306.Village306;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(StructureStart.class)
public abstract class UndergroundStart306Mixin {
@WrapMethod(method = "placeInChunk")
private void sanctuary$projectVillage306(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, Operation<Void> original) {
Village306.placing( (StructureStart) (Object) this,
() -> original.call(level, manager, generator, random, clip, chunk));
}
}
@@ -21,6 +21,10 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|| !fr.koka.sanctuary.expansion.ExpansionIsland.spatialGeneration(generator.rootGeneration())) return;
// Aerial pieces write in genuine void chunks, within their own immutable site and current chunk only.
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return; // Vanilla still applies its write-zone and build-height checks.
@@ -44,6 +48,10 @@ public abstract class UnifiedDecorationProtectionMixin {
if (!(region.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)) return;
// Vanilla rejects writes outside its region before reading the destination chunk.
if (!region.isWithinWriteZone(pos)) return;
if (fr.koka.sanctuary.worldgen.underground306.Village306.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.underground306.Village306.mayWrite(pos)) callback.setReturnValue(false);
return;
}
if (generator.rootGeneration() == 24 && fr.koka.sanctuary.worldgen.aerial.AerialStructures24.isPlacing()) {
if (!fr.koka.sanctuary.worldgen.aerial.AerialStructures24.mayWrite(pos)) callback.setReturnValue(false);
return;
@@ -15,7 +15,7 @@ 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 static final int MAX_ATTEMPTS = 32;
private CavityMonsterRoom28() {}
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
@@ -24,10 +24,9 @@ public final class CavityMonsterRoom28 {
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}) {
for (int dx : new int[]{4, 8, 11}) for (int dz : new int[]{4, 8, 11}) {
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
for (int y = bottom; y <= top; y++) {
BlockPos at = new BlockPos(x, y, z);
@@ -46,7 +45,17 @@ public final class CavityMonsterRoom28 {
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;
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) {
// This late native room can carve a floor below vegetation placed earlier.
// Native generation uses quiet writes, so settle those plants locally, without drops.
for(int dy=-1;dy<=5;dy++)for(int dx=-4;dx<=4;dx++)for(int dz=-4;dz<=4;dz++) {
var plant=at.offset(dx,dy,dz);var state=level.getBlockState(plant);
if((state.getBlock() instanceof net.minecraft.world.level.block.VegetationBlock
|| state.getBlock() instanceof net.minecraft.world.level.block.SugarCaneBlock)
&& !state.canSurvive(level,plant)) level.setBlock(plant,Blocks.AIR.defaultBlockState(),2);
}
return;
}
}
}
@@ -14,15 +14,20 @@ public final class DetachedRock302 implements DensitySampler {
private final double radius;
private final long seed;
private final boolean broad;
private final int ceiling;
public DensitySampler input() { return input; }
public DetachedRock302(DensitySampler input, double radius, long seed) {
this(input, radius, seed, false);
}
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad) {
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad;
this(input,radius,seed,broad,376);
}
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad, int ceiling) {
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad; this.ceiling=ceiling;
}
private boolean affected(int x, int y, int z) {
if (broad) return y > 170 && y < 376 && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
if (broad) return y > 170 && y < ceiling && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
return SurfaceRelief302.weight(x,y,z,radius) * Relief30.influence(seed,x,z,radius) > 0;
}
@Override public float sampleValue(SamplerContext context,int x,int y,int z) {
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import fr.koka.sanctuary.SanctuaryMod;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** A few solid, eroded rock bodies in the upper air, not a field of floating debris. */
public final class FloatingTerrain306 implements DensitySampler {
public record Mass(int x, int y, int z, int rx, int rz, int depth, int rise) {}
private final DensitySampler ground, coarse;
private final double radius;
private final long seed;
private volatile List<Mass> masses;
public FloatingTerrain306(DensitySampler ground, DensitySampler coarse, double radius, long seed) {
this.ground=ground; this.coarse=coarse; this.radius=radius; this.seed=seed;
}
public List<Mass> masses(SamplerContext sample) {
if(masses!=null) return masses;
synchronized(this) {
if(masses!=null) return masses;
var random=new Random(seed ^ 0x306A1B0D1EL); var chosen=new ArrayList<Mass>();
int count=radius>=500?3:2;
double phase=random.nextDouble()*Math.PI*2;
for(int i=0;i<count;i++) for(int attempt=0;attempt<24;attempt++) {
double angle=phase+i*Math.PI*2/count+(random.nextDouble()-.5)*.6;
double distance=radius*(.40+random.nextDouble()*.22);
var m=new Mass((int)(Math.cos(angle)*distance),352+random.nextInt(20),
(int)(Math.sin(angle)*distance),25+random.nextInt(12),22+random.nextInt(12),25+random.nextInt(10),9+random.nextInt(9));
boolean clear=true;
for(var other:chosen) if(Math.hypot(m.x-other.x,m.z-other.z)<m.rx+other.rx+36) {clear=false;break;}
// Probe the existing field only. No terrain edits or chunk loads to make a site fit.
for(int z=m.z-m.rz*2;clear&&z<=m.z+m.rz*2;z+=6)
for(int x=m.x-m.rx*2;clear&&x<=m.x+m.rx*2;x+=6)
for(int y=m.y-m.depth*2-18;y<=m.y+m.rise*2;y+=4)
if(ground.sampleValue(sample,x,y,z)>0) {clear=false;break;}
if(clear) {chosen.add(m);break;}
}
masses=List.copyOf(chosen);
SanctuaryMod.LOGGER.info("Floating terrain306 radius {}: {} / {} coherent bodies",radius,masses.size(),count);
return masses;
}
}
@Override public float sampleValue(SamplerContext sample,int x,int y,int z) {
float result=ground.sampleValue(sample,x,y,z);
if(y<300||y>=400||Math.hypot(x,z)>radius*.85) return result;
for(var m:masses(sample)) {
double dx=(x-m.x)/(double)m.rx, dz=(z-m.z)/(double)m.rz;
if(Math.abs(dx)>1.8||Math.abs(dz)>1.8||y<m.y-m.depth*1.5||y>m.y+m.rise*1.5) continue;
// Low-frequency 3D erosion changes the silhouette; a solid centre cannot become a shell.
double noise=Math.clamp(2*coarse.sampleValue(sample,x+457,y-231,z-619),-1,1);
dx += .24*Math.clamp(2*coarse.sampleValue(sample,x-331,y+179,z+223),-1,1);
dz += .24*Math.clamp(2*coarse.sampleValue(sample,x+613,y-71,z-337),-1,1);
double edge=1-dx*dx-dz*dz+.72*noise;
double vertical=y<m.y ? (m.y-y)/(double)m.depth : (y-m.y)/(double)m.rise;
double body=edge-vertical*vertical;
result=Math.max(result,(float)Math.clamp(body*.30,-1,1));
}
return result;
}
@Override public void sampleVolume(SamplerContext sample,DensityBuffer buffer,DensityVolume volume) {
DensitySampler.sampleVolumeNaive(sample,buffer,volume,this);
}
}
@@ -0,0 +1,61 @@
package fr.koka.sanctuary.worldgen;
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
/** Broad foothills around native geological provinces. No density threshold cuts their sides. */
public final class HighPlateaus304 {
private static final int SIZE = 65;
private final float[] foothills = new float[SIZE * SIZE];
private final double radius;
public HighPlateaus304(DensitySampler noise, double radius, long seed) {
this.radius = radius;
var original = new BalancedRelief303(noise, radius, seed);
float[] provinces = new float[SIZE * SIZE];
double step = radius / 32, width = Math.clamp(radius * .25, 64, 96);
for (int z = 0; z < SIZE; z++) for (int x = 0; x < SIZE; x++)
provinces[z * SIZE + x] = (float) original.province(
(int)Math.round((x - 32) * step), (int)Math.round((z - 32) * step));
int reach = (int)Math.ceil(width / step);
for (int z = 0; z < SIZE; z++) for (int x = 0; x < SIZE; x++) {
double value = provinces[z * SIZE + x];
for (int dz = -reach; dz <= reach; dz++) for (int dx = -reach; dx <= reach; dx++) {
int nx = x + dx, nz = z + dz;
if (nx < 0 || nz < 0 || nx >= SIZE || nz >= SIZE) continue;
double peak = provinces[nz * SIZE + nx];
if (peak < .9) continue;
value = Math.max(value, peak * smooth(1 - Math.hypot(dx, dz) * step / width));
}
foothills[z * SIZE + x] = (float)value;
}
}
public double province(int x, int z) {
double gx = Math.clamp((x / radius + 1) * 32, 0, 63.999999);
double gz = Math.clamp((z / radius + 1) * 32, 0, 63.999999);
int ix = (int)gx, iz = (int)gz;
double fx = gx - ix, fz = gz - iz;
double value = lerp(fz, lerp(fx, foothills[iz*SIZE+ix], foothills[iz*SIZE+ix+1]),
lerp(fx, foothills[(iz+1)*SIZE+ix], foothills[(iz+1)*SIZE+ix+1]));
// Keep arrival and the transit plateau; their transitions also have a physical width.
double minor = Math.max(20, radius * .08);
double ellipse = Math.hypot(x / (radius * .62), (z + radius * .25) / minor);
return value * smooth((Math.hypot(x,z) - 48) / 64)
* smooth((ellipse - 1) * minor / 32);
}
public static double sourceY(int y, double province) {
return BalancedRelief303.sourceY(y, province);
}
public static double roofErosion(int y, double regionalNoise) {
double roof = 357 + 2 * Math.tanh(regionalNoise * 2);
return 2 * smooth((y - (roof - 3)) / 6);
}
private static double lerp(double t, double a, double b) { return a + t * (b - a); }
public static double smooth(double t) {
t = Math.clamp(t, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -269,7 +269,10 @@ public final class PopulationHydrology {
}
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
return new Planner(seed, sampler, false).build();
}
public static Plan withHighPlateaus304(long seed, Sampler sampler) {
return new Planner(seed, sampler, true).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,
@@ -285,6 +288,7 @@ public final class PopulationHydrology {
private static final class Planner {
private final long seed;
private final Sampler sampler;
private final boolean highPlateaus;
private final Map<Long, Column> columns = new HashMap<>();
private final Map<Long, Integer> coarse = new HashMap<>();
private final Map<Long, Cell> cells = new HashMap<>();
@@ -293,11 +297,14 @@ public final class PopulationHydrology {
private final Set<Position> springFlow = new HashSet<>();
private long samples;
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
Planner(long seed, Sampler sampler, boolean highPlateaus) {
this.seed = seed; this.sampler = sampler; this.highPlateaus = highPlateaus;
}
Plan build() {
List<Candidate> candidates = candidates();
addRiver();
if (highPlateaus) addRiver(true);
addRiver(false);
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 4 || attempts++ >= 96) break;
@@ -305,7 +312,10 @@ public final class PopulationHydrology {
if (nearWater(center.x(), center.z(), 38)) continue;
Column column = column(center.x(), center.z());
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
// An upper river must not consume the lake's slot merely by incrementing the feature count.
boolean lake = highPlateaus ? features.stream().noneMatch(f -> f.kind() == Kind.LAKE)
: features.size() < 2;
Kind kind = lake ? Kind.LAKE : Kind.POND;
int target = kind == Kind.LAKE ? 700 + (int) (random(seed, center.x(), center.z()) * 400)
: 100 + (int) (random(seed + 1, center.x(), center.z()) * 140);
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
@@ -331,8 +341,8 @@ public final class PopulationHydrology {
/** A connected catchment is excavated only in a thick natural plateau. The route is
* selected on the actual fissured density, not painted across missing rock. Its two
* terminal pools share the same retained waterline; a separate rock spring feeds it. */
private void addRiver() {
List<Integer> heights = coarse.values().stream().filter(y -> y >= 160).sorted().toList();
private void addRiver(boolean upper) {
List<Integer> heights = coarse.values().stream().filter(y -> upper ? y >= 330 : y >= 160 && (!highPlateaus || y < 330)).sorted().toList();
if (heights.isEmpty()) return;
Set<Integer> levels = new java.util.LinkedHashSet<>();
for (double quantile : new double[]{.55, .35, .75, .15, .90}) {
@@ -351,9 +361,9 @@ public final class PopulationHydrology {
}
for (int attempt = 0; attempt < 4 && allowed.size() > 100; attempt++) {
List<Point> route = longestRoute(allowed, level);
if (route.size() < 36) break;
if (route.size() < (upper ? 18 : 36)) break;
Point first = route.getFirst(), last = route.getLast();
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < 100) break;
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < (upper ? 52 : 100)) break;
List<Point> path = smoothRoute(route);
Set<Long> wet = new HashSet<>();
Set<Long> bad = new HashSet<>();
@@ -372,7 +382,7 @@ public final class PopulationHydrology {
boolean pools = true;
for (Point endpoint : List.of(first, last)) {
Set<Long> pool = growRiverPool(endpoint, level);
if (pool.size() < 90) { pools = false; break; }
if (pool.size() < (upper ? 40 : 90)) { pools = false; break; }
wet.addAll(pool);
}
if (!pools) { allowed.remove(key(first.x(), first.z())); allowed.remove(key(last.x(), last.z())); continue; }
@@ -421,6 +431,10 @@ public final class PopulationHydrology {
}
private boolean riverRock(int x, int z, int level) {
if (highPlateaus && !features.isEmpty()) {
for (int dx : new int[]{-4, 0, 4}) for (int dz : new int[]{-4, 0, 4})
if (cells.containsKey(key(x + dx, z + dz))) return false;
}
Column rock = column(x, z);
return rock.top() >= level && rock.top() <= level + 12 && rock.solidBottom() <= level - 8;
}
@@ -49,6 +49,14 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
}
@Override public DensitySampler compileSampler(CompileContext context) {
return compileSampler(context, false, .95);
}
/** Exact rejected30.4 field retained for the reproducible scientific comparison. */
public DensitySampler compileHistorical304(CompileContext context) {
return compileSampler(context, true, .8);
}
public DensitySampler compileHistorical305(CompileContext context) { return compileSampler(context, false, .8); }
private DensitySampler compileSampler(CompileContext context, boolean historical304, double plateauStrength) {
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);
@@ -57,7 +65,10 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
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 balanced = balanced303 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null;
var balanced = balanced303 && historical304 ? new HighPlateaus304(coarse, capacity.radius(), seed) : null;
// A broad mask changes the strength of the native 3D deformation only. No Y stretch,
// union of two shells, summit cap or fine surface noise in the current profile.
var plateaus = balanced303 && !historical304 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null;
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
DensitySampler field = new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) {
@@ -81,9 +92,10 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
float fineValue = fine.sampleValue(sample, x, localY, z);
float terrainValue;
double province = balanced != null ? balanced.province(x, z) : 0;
double mappedY = balanced != null ? BalancedRelief303.sourceY(y, province) : y;
double mappedY = balanced != null && province > 0 ? HighPlateaus304.sourceY(y, province) : y;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (balanced != null) weight *= province;
if (plateaus != null) weight *= plateauStrength * plateaus.province(x, z);
else if (balanced != null) weight *= province;
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
@@ -103,7 +115,14 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
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, mappedY - 64);
if (balanced != null) natural = Math.max(-1, natural - (float) BalancedRelief303.ceilingErosion(y));
if (balanced != null && province > 0 && y > 170) {
float base = PopulationIslandShape.raisedDensity(x, y, z,
raw.sampleValue(sample, x, localY, z), edge.sampleValue(sample, x, 0, z),
coarseValue, fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
double roofNoise = coarse.sampleValue(sample, x / 2, 91, z / 2);
natural = Math.max(base, (float) Math.max(-1, natural
- HighPlateaus304.roofErosion(y, roofNoise)));
}
if (natural <= 0) return natural;
var region = capacity.regionAt(x, z);
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
@@ -122,7 +141,9 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303) : field;
if (balanced303 && !historical304 && plateauStrength > .9)
field = new FloatingTerrain306(field, coarse, capacity.radius(), seed);
return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303, field instanceof FloatingTerrain306 ? 412 : 376) : field;
}
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() {
@@ -137,6 +137,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
lostCityPlan(island);
return cityPlacements21.get(cityIdentity(island));
}
private volatile fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306 = fr.koka.sanctuary.worldgen.underground306.Village306.Plan.EMPTY;
private boolean villagePrepared306;
private boolean minesPrepared306;
private volatile java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306 = java.util.List.of();
public java.util.List<fr.koka.sanctuary.expansion.ExpansionStructures23.Accepted> starterMines306() { return starterMines306; }
public fr.koka.sanctuary.worldgen.underground306.Village306.Plan village306() { return village306; }
private volatile Context originContext;
private final int generation;
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
@@ -178,6 +184,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
}
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 24 ? AERIAL24_CODEC : generation == 23 ? SECRETS23_CODEC : generation == 22 ? SELECTIVE22_CODEC : generation == 21 ? NETWORK21_CODEC : generation == 20 ? ARCHITECTURE20_CODEC : generation == 19 ? HERITAGE19_CODEC : generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
@Override public int getGenDepth() { return generation == 24 ? 640 : super.getGenDepth(); }
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
}
@@ -188,6 +195,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
public fr.koka.sanctuary.worldgen.heritage.HeritagePlan19 heritagePlan19() { return heritagePlan19; }
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
if (village306.intersects(minX,minY,minZ,maxX,maxY,maxZ)) return true;
for (var mine : starterMines306) for(var piece : mine.start().getPieces())
if(piece.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(minX,minY,minZ,maxX,maxY,maxZ))) return true;
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.minY() - 1) return true;
@@ -245,6 +255,14 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
prepareRuins(islands);
prepareHeritage(islands);
prepareTransit(islands);
if (!villagePrepared306 && generation == 24 && cityStructuresEnabled && experimentalStructures) {
villagePrepared306 = true;
village306 = fr.koka.sanctuary.worldgen.underground306.VillagePlanner306.plan(level, this);
}
if (!minesPrepared306 && generation == 24 && cityStructuresEnabled) {
minesPrepared306 = true;
starterMines306 = fr.koka.sanctuary.expansion.ExpansionStructures23.starterMines306(level,this,islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow());
}
}
@Override public void setIslands(List<ExpansionIsland> islands) {
// Complete and publish city geometry before workers can observe the new owner list.
@@ -319,7 +337,9 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
sampled.discoverLandmarks(lostCityPlan(root), ruinsPlan17);
var plan = (generation == 23 || generation == 24)
var plan = generation == 24
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan306(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation == 23
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation == 22
? fr.koka.sanctuary.worldgen.heritage.HeritagePlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
@@ -549,6 +569,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
});
}
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
if (village306.cavern() != null && village306.cavern().getBoundingBox().isInside(pos)) return true;
if (generation == 24 && aerialPlan24.protects(pos)) return true;
if ((generation == 23 || generation == 24) && mineralSanctuary23.protects(pos.getX(), pos.getY(), pos.getZ())) return true;
if (generation == 16 || generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
@@ -17,7 +17,7 @@ public final class WoodlandGroveGeometry {
}
public static boolean naturalFootprint(Sampler sampler, int x, int bedY, int z, int width) {
if (bedY < 32 || bedY > 350) return false;
if (bedY < 32 || bedY > 360) return false;
for (int dx = 0; dx < width; dx++) for (int dz = 0; dz < width; dz++) {
for (int dy = -2; dy <= 0; dy++) if (sampler.sample(x + dx, bedY + dy, z + dz) <= 0) return false;
if (sampler.sample(x + dx, bedY + 1, z + dz) > 0) return false;
@@ -26,8 +26,14 @@ public final class HeritagePlanner23 {
private HeritagePlanner23() {}
public static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
return plan(seed, cx, cz, diameter, terrain, false);
}
public static HeritagePlan19 plan306(long seed, int cx, int cz, int diameter, Terrain terrain) {
return plan(seed, cx, cz, diameter, terrain, true);
}
private static HeritagePlan19 plan(long seed, int cx, int cz, int diameter, Terrain terrain, boolean branching) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Heritage23 requires a starter size");
var probe = new Probe(terrain, cx, cz, diameter);
var probe = new Probe(terrain, cx, cz, diameter); probe.branching = branching;
probe.columnLimit = SITE_COLUMNS; probe.sampleLimit = SITE_SAMPLES;
List<Landmark> landmarks = new ArrayList<>(List.copyOf(terrain.landmarks()).stream().limit(12).toList());
naturalRailEndpoints(seed, cx, cz, diameter, landmarks, probe);
@@ -54,7 +60,8 @@ public final class HeritagePlanner23 {
}
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var links = new ArrayList<Link>();
naturalRailFragments(seed, landmarks, sites, links, cells, probe);
if (branching) branchingRails306(seed, diameter, landmarks, sites, links, cells, probe);
else naturalRailFragments(seed, landmarks, sites, links, cells, probe);
probe.columnLimit = MAX_COLUMNS; probe.sampleLimit = MAX_SAMPLES;
var routeResults = new LinkedHashMap<String, String>();
var routeReasons = new java.util.TreeMap<String, Integer>(); probe.reasons = routeReasons;
@@ -152,8 +159,8 @@ public final class HeritagePlanner23 {
List<Landmark> landmarks, Probe p) {
try {
double axis = unit(mix(seed ^ 0x23A115L)) * Math.PI * 2;
for (int end = 0; end < 2; end++) for (int attempt = 0; attempt < 16; attempt++) {
double angle = axis + end * Math.PI + (attempt % 2 == 0 ? 1 : -1) * (attempt / 2) * .07;
for (int end = 0; end < (p.branching ? 4 : 2); end++) for (int attempt = 0; attempt < 16; attempt++) {
double angle = axis + end * (p.branching ? Math.PI / 2 : Math.PI) + (attempt % 2 == 0 ? 1 : -1) * (attempt / 2) * .07;
double radius = diameter * (attempt < 8 ? .25 : .18);
int x = cx + (int) Math.round(Math.cos(angle) * radius);
int z = cz + (int) Math.round(Math.sin(angle) * radius);
@@ -214,6 +221,69 @@ public final class HeritagePlanner23 {
} catch (BudgetExceeded exhausted) { p.reject("fragment-budget"); }
}
/** Grow along locally feasible grades, then fork from an actual saved endpoint. */
private static void branchingRails306(long seed, int diameter, List<Landmark> landmarks, List<Site> sites,
List<Link> links, Map<Long, OwnedCell> cells, Probe p) {
p.columnLimit = MAX_COLUMNS - 1_500; p.sampleLimit = MAX_SAMPLES - 80_000;
record Arm(Landmark from, int facing, int depth) {}
var queue = new java.util.ArrayDeque<Arm>();
for (var l : landmarks) if (l.id().startsWith("natural_rail_")) queue.add(new Arm(l,l.entry().facing(),0));
int serial = 0;
try {
while (!queue.isEmpty() && landmarks.size() < 16 && links.size() < 20) {
var arm=queue.removeFirst(); var from=arm.from();
var path=new ArrayList<XZ>(); path.add(new XZ(from.entry().x(),from.entry().z()));
RouteDraft best=null; int face=arm.facing();
String id="rail_branch_"+serial;
var request=new Request("branch_"+serial,from.id(),id,RouteKind.FREIGHT);
int target=diameter/4;
for(int step=0;step<target/8;step++) {
RouteDraft chosen=null; List<XZ> chosenPath=null; int chosenFace=face;
var last=path.getLast();
for(int turn:new int[]{0,1,-1}) {
int direction=(face+turn+4)&3;
// Give long straight runs priority, with curves only where the grade needs them.
var end=last.move(direction,8);int top=p.surface(end.x(),end.z());
if(top<14||top>350)continue;
if(path.stream().anyMatch(at->Math.abs(at.x()-end.x())+Math.abs(at.z()-end.z())<5))continue;
if(cells.containsKey(key(end.x(),end.z())))continue;
var proposed=new ArrayList<>(path);line(proposed,end);
var endEntry=new Entry(end.x(),top+1,end.z(),(direction+2)&3);
var historical=landmarks.stream().filter(l->!l.id().startsWith("natural_rail_")&&!l.id().startsWith("rail_branch_")).toList();
var draft=profile(seed,request,new Endpoint(from.id(),from.entry(),false),new Endpoint(id,endEntry,false),proposed,sites,historical,cells,p);
if(draft!=null){chosen=draft;chosenPath=proposed;chosenFace=direction;break;}
}
if(chosen==null)break;
best=chosen;path=new ArrayList<>(chosenPath);face=chosenFace;
}
if(best==null||best.path().size()<17)continue;
var end=best.path().getLast();
var terminal=new Landmark(id,LandmarkKind.MINE,new Entry(end.x(),end.floorY()+1,end.z(),face));
landmarks.add(terminal);
if(best.path().size() >= 49 && landmarks.size() < 15 && links.size() < 19) {
int middle=best.path().size()/2;var at=best.path().get(middle);var after=best.path().get(middle+1);
int tangent=direction(at.x(),at.z(),after.x(),after.z());
var fork=new Landmark(id+"_fork",LandmarkKind.MINE,new Entry(at.x(),at.floorY()+1,at.z(),tangent));
landmarks.add(fork);
links.add(new Link(request.id()+"_a",request.from(),fork.id(),request.kind(),best.path().subList(0,middle+1)));
links.add(new Link(request.id()+"_b",fork.id(),request.to(),request.kind(),best.path().subList(middle,best.path().size())));
queue.addFirst(new Arm(fork,(tangent+1)&3,arm.depth()+1));
queue.addLast(new Arm(fork,(tangent+3)&3,arm.depth()+1));
} else links.add(new Link(request.id(),request.from(),request.to(),request.kind(),best.path()));
for(var cell:best.cells().values()) {
long key=key(cell.x(),cell.z());var previous=cells.get(key);
if(previous==null)cells.put(key,new OwnedCell(request,cell));
else if(cell.rail()!=Rail.NONE && previous.cell().rail()!=cell.rail()) {
var c=previous.cell();cells.put(key,new OwnedCell(previous.request(),new Cell(c.x(),c.floorY(),c.z(),c.foundationDepth(),cell.rail(),false)));
}
}
serial++;
// Forward continuation and a lateral branch share their ballast; the switch is repairable.
if(arm.depth()<2){queue.addFirst(new Arm(terminal,(face+(serial%2==0?1:3))&3,arm.depth()+1));queue.addLast(new Arm(terminal,face,arm.depth()+1));}
}
} catch(BudgetExceeded exhausted){p.reject("branched-rail-budget");}
}
private static List<Candidate> candidates(long seed, Kind kind, int cx, int cz, int diameter,
List<Site> sites, List<Landmark> landmarks, Probe probe) {
double angle = unit(mix(seed ^ 0x19ACEL)) * Math.PI * 2 + kind.ordinal() * 2.399963229728653;
@@ -665,7 +735,7 @@ public final class HeritagePlanner23 {
private static final class Probe {
final Terrain terrain; final int cx, cz; final double radius;
final Map<Long, Integer> columns = new HashMap<>();
XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
boolean branching; XZ routeBlock; Map<String, Integer> reasons = new HashMap<>(); int samples, columnLimit, sampleLimit;
Probe(Terrain terrain, int cx, int cz, int diameter) { this.terrain = java.util.Objects.requireNonNull(terrain); this.cx = cx; this.cz = cz; radius = diameter / 2.0 - 2; }
int surface(int x, int z) {
if (Math.hypot(x - cx, z - cz) < 48) return INVALID;
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.worldgen.underground306;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.List;
import java.util.Map;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureType;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
public final class Village306 {
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("underground_abandoned_village"));
public static final StructureType<VillageStructure306> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("underground_site_v306"), () -> VillageStructure306.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
SanctuaryMod.id("village_cavern_v306"), VillageCavern306::new);
public record Plan(StructureStart start, VillageCavern306 cavern, Map<String,Integer> diagnostics) {
public static final Plan EMPTY = new Plan(null, null, Map.of());
public boolean intersects(int x0,int y0,int z0,int x1,int y1,int z1) {
return cavern != null && cavern.getBoundingBox().intersects(new net.minecraft.world.level.levelgen.structure.BoundingBox(x0,y0,z0,x1,y1,z1));
}
}
private static final ThreadLocal<VillageCavern306> PLACING = new ThreadLocal<>();
public static void register() {}
public static void placing(StructureStart start, Runnable action) {
VillageCavern306 cavity = start.getPieces().stream().filter(VillageCavern306.class::isInstance)
.map(VillageCavern306.class::cast).findFirst().orElse(null);
var previous = PLACING.get();
if (cavity != null) PLACING.set(cavity);
try { action.run(); } finally { if (previous == null) PLACING.remove(); else PLACING.set(previous); }
}
public static boolean isPlacing() { return PLACING.get() != null; }
public static boolean mayWrite(net.minecraft.core.BlockPos pos) { var c = PLACING.get(); return c != null && c.getBoundingBox().isInside(pos); }
public static Integer projectedHeight(int x,int z) { var c = PLACING.get(); return c != null && c.projects(x,z) ? c.floorY() : null; }
private Village306() {}
}
@@ -0,0 +1,106 @@
package fr.koka.sanctuary.worldgen.underground306;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** Saved local cavern. All houses, roads, chests and inhabitants are native village pieces. */
public final class VillageCavern306 extends StructurePiece {
private final int floorY;
private final long seed;
private final BlockPos entrance;
private final List<BoundingBox> volumes;
public VillageCavern306(int floorY, long seed, BlockPos entrance, List<BoundingBox> volumes) {
super(Village306.PIECE, 0, bounds(volumes));
this.floorY = floorY; this.seed = seed; this.entrance = entrance.immutable();
this.volumes = List.copyOf(volumes); setOrientation(null); validate();
}
public VillageCavern306(StructurePieceSerializationContext context, CompoundTag tag) {
super(Village306.PIECE, tag);
if (tag.getIntOr("Schema", 0) != 1) throw new IllegalArgumentException("Unknown underground306 piece schema");
floorY = tag.getInt("Floor").orElseThrow();
seed = tag.getLong("Seed").orElseThrow();
entrance = new BlockPos(tag.getInt("EntryX").orElseThrow(), tag.getInt("EntryY").orElseThrow(), tag.getInt("EntryZ").orElseThrow());
List<BoundingBox> decoded = new ArrayList<>();
ListTag list = tag.getList("Volumes").orElseThrow();
if (list.isEmpty() || list.size() > 512) throw new IllegalArgumentException("Invalid underground306 volume count");
for (int i = 0; i < list.size(); i++) {
var t = list.getCompound(i).orElseThrow();
decoded.add(new BoundingBox(t.getInt("x0").orElseThrow(), t.getInt("y0").orElseThrow(), t.getInt("z0").orElseThrow(),
t.getInt("x1").orElseThrow(), t.getInt("y1").orElseThrow(), t.getInt("z1").orElseThrow()));
}
volumes = List.copyOf(decoded);
setOrientation(null); validate();
if (!getBoundingBox().equals(bounds(volumes))) throw new IllegalArgumentException("Underground306 piece bounds mismatch");
}
private void validate() {
if (floorY < 3 || floorY > 345 || volumes.isEmpty() || volumes.size() > 512
|| getBoundingBox().getXSpan() > 256 || getBoundingBox().getZSpan() > 256
|| getBoundingBox().minY() < 1 || getBoundingBox().maxY() > 358)
throw new IllegalArgumentException("Invalid underground306 geometry");
}
public static BoundingBox bounds(List<BoundingBox> volumes) {
return BoundingBox.encapsulatingBoxes(volumes).orElseThrow();
}
public int floorY() { return floorY; }
public long seed() { return seed; }
public List<BoundingBox> volumes() { return volumes; }
@Override public BlockPos getLocatorPosition() { return entrance; }
public boolean projects(int x, int z) {
return roofAt(x, z) >= floorY + 2;
}
/** Broad rounded chambers overlap around the complete native graph; no global cave field. */
public int roofAt(int x, int z) {
int roof = -1;
for (var b : volumes) {
double dx = Math.max(0, Math.max(b.minX() + 8 - x, x - (b.maxX() - 8))) / 8.0;
double dz = Math.max(0, Math.max(b.minZ() + 8 - z, z - (b.maxZ() - 8))) / 8.0;
double edge = dx * dx + dz * dz;
if (edge > 1) continue;
double ripple = Math.sin((x + (seed & 255)) * .071) * Math.cos(z * .063) * 2;
roof = Math.max(roof, b.maxY() - 3 + (int) Math.round(ripple - edge * 5));
}
return roof;
}
public BlockState terrainOverride(int x, int y, int z) {
if (!getBoundingBox().isInside(x, y, z) || roofAt(x,z) < floorY + 2 || y > roofAt(x,z)) return null;
if (y < floorY - 1) return Blocks.STONE.defaultBlockState();
if (y == floorY - 1) return Blocks.DIRT.defaultBlockState();
return Blocks.CAVE_AIR.defaultBlockState();
}
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
tag.putInt("Schema", 1); tag.putInt("Floor", floorY); tag.putLong("Seed", seed);
tag.putInt("EntryX", entrance.getX()); tag.putInt("EntryY", entrance.getY()); tag.putInt("EntryZ", entrance.getZ());
ListTag list = new ListTag();
for (var b : volumes) {
var t = new CompoundTag(); t.putInt("x0", b.minX()); t.putInt("y0", b.minY()); t.putInt("z0", b.minZ());
t.putInt("x1", b.maxX()); t.putInt("y1", b.maxY()); t.putInt("z1", b.maxZ()); list.add(t);
}
tag.put("Volumes", list);
}
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
var b = getBoundingBox();
for (int z = Math.max(b.minZ(), clip.minZ()); z <= Math.min(b.maxZ(), clip.maxZ()); z++)
for (int x = Math.max(b.minX(), clip.minX()); x <= Math.min(b.maxX(), clip.maxX()); x++)
for (int y = b.minY(); y <= b.maxY(); y++) {
var block = terrainOverride(x, y, z);
// Keep existing geology under the walking floor; only bridge local cracks.
if (block != null && !(y < floorY - 1 && level.getBlockState(new BlockPos(x,y,z)).isSolid()))
placeBlock(level, block, x, y, z, clip);
}
}
}
@@ -0,0 +1,173 @@
package fr.koka.sanctuary.worldgen.underground306;
import com.google.gson.JsonObject;
import com.mojang.serialization.JsonOps;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.*;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
/** Count-bounded search, independent of loaded chunks and machine speed. */
public final class VillagePlanner306 {
private final SanctuaryChunkGenerator host;
private final ServerLevel level;
private final ExpansionIsland root;
private final Map<Long, NoiseColumn> columns = new HashMap<>();
private final Map<String,Integer> diagnostics = new LinkedHashMap<>();
private static final int MAX_COLUMNS = 32000;
private VillagePlanner306(ServerLevel level, SanctuaryChunkGenerator host) {
this.level=level; this.host=host; this.root=host.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
}
public static Village306.Plan plan(ServerLevel level, SanctuaryChunkGenerator host) {
var p=new VillagePlanner306(level,host); long began=System.nanoTime();
Village306.Plan result;
try { result=p.search(); } catch (BudgetExceeded exhausted) { p.reject("column-budget"); result=new Village306.Plan(null,null,Map.copyOf(p.diagnostics)); }
SanctuaryMod.LOGGER.info("Village306 seed {} size {}: {}, {} columns, {} ms; {}", level.getSeed(),host.initialDiameter(),
result.start()==null ? "REFUSED" : result.start().getBoundingBox(),p.columns.size(),(System.nanoTime()-began)/1000000,result.diagnostics());
return result;
}
private void reject(String why) { diagnostics.merge(why,1,Integer::sum); }
private static final class BudgetExceeded extends RuntimeException {}
private NoiseColumn column(int x,int z) {
long key=BlockPos.asLong(x,0,z); var found=columns.get(key);
if(found==null) { if(columns.size()>=MAX_COLUMNS) throw new BudgetExceeded(); found=host.getBaseColumn(x,z,level,host.randomState(root)); columns.put(key,found); }
return found;
}
private boolean solid(int x,int y,int z) { var b=column(x,z).getBlock(y); return b.isSolid() && b.getFluidState().isEmpty(); }
private Village306.Plan search() {
// Prefer the island's interior rock, not its arrival or thin outer rim.
var candidates=new ArrayList<ChunkPos>();
double phase=(mix(root.seed()) & 65535)*Math.PI*2/65536;
for(int ring=0;ring<3;ring++) for(int angle=0;angle<8;angle++) {
double a=phase+angle*Math.PI/4+ring*.19, radius=host.initialDiameter()*(.12+ring*.055);
candidates.add(new ChunkPos((int)Math.round(Math.cos(a)*radius)>>4,(int)Math.round(Math.sin(a)*radius)>>4));
}
for(int ring=0;ring<3;ring++) for(int angle=0;angle<12;angle++) {
double a=phase+angle*Math.PI/6+ring*.17, radius=host.initialDiameter()*(.27+ring*.055);
var at=new ChunkPos((int)Math.round(Math.cos(a)*radius)>>4,(int)Math.round(Math.sin(a)*radius)>>4);
if(!candidates.contains(at))candidates.add(at);
}
for(var chunk:candidates) {
// Full graph once per position, never reduced to fit a cavity.
for(int floor:new int[]{112,144,80,176,208,192}) {
reject("attempts");
var start=nativeStart(chunk,floor);
if(!start.isValid()) {reject("native-refused");continue;}
long houses=start.getPieces().stream().filter(p->p instanceof PoolElementStructurePiece pool && pool.getElement().toString().contains("/houses/")).count();
if(houses<6) {reject("too-few-houses");break;}
var volumes=new ArrayList<BoundingBox>();
for(var piece:start.getPieces()) {
var b=piece.getBoundingBox();
volumes.add(new BoundingBox(b.minX()-10,Math.min(floor-10,b.minY()-2),b.minZ()-10,b.maxX()+10,Math.max(floor+19,b.maxY()+12),b.maxZ()+10));
}
var bounds=VillageCavern306.bounds(volumes);
if(bounds.getXSpan()>240 || bounds.getZSpan()>240 || bounds.minX()<chunk.getMinBlockX()-128 || bounds.maxX()>chunk.getMaxBlockX()+128
|| bounds.minZ()<chunk.getMinBlockZ()-128 || bounds.maxZ()>chunk.getMaxBlockZ()+128) {reject("reference-range");continue;}
if(host.intersectsExperimentalStructures(bounds.minX(),bounds.minY(),bounds.minZ(),bounds.maxX(),bounds.maxY(),bounds.maxZ())) {reject("reserved");continue;}
boolean outside=false;
for(var b:volumes) if(!root.contains(b.minX(),b.minZ()) || !root.contains(b.maxX(),b.maxZ())) {outside=true;break;}
if(outside) {reject("outside-island");continue;}
var cavity=new VillageCavern306(floor,mix(root.seed()^chunk.pack()),new BlockPos(chunk.getMiddleBlockX(),floor,chunk.getMiddleBlockZ()),volumes);
String refusal=certify(cavity);
if(refusal!=null) {reject(refusal);continue;}
var entrance=access(start,cavity,volumes,chunk);
if(entrance==null) {reject("no-access");continue;}
cavity=new VillageCavern306(floor,mix(root.seed()^chunk.pack()),entrance,volumes);
var completed=cavity.getBoundingBox();
if(host.intersectsExperimentalStructures(completed.minX(),completed.minY(),completed.minZ(),completed.maxX(),completed.maxY(),completed.maxZ())
|| !root.contains(completed.minX(),completed.minZ()) || !root.contains(completed.maxX(),completed.maxZ())
|| completed.minX()<chunk.getMinBlockX()-128 || completed.maxX()>chunk.getMaxBlockX()+128
|| completed.minZ()<chunk.getMinBlockZ()-128 || completed.maxZ()>chunk.getMaxBlockZ()+128) {reject("access-reservation");continue;}
if(certify(cavity)!=null) {reject("access-terrain");continue;}
List<StructurePiece> pieces=new ArrayList<>(); pieces.add(cavity);pieces.addAll(start.getPieces());
var complete=new StructureStart(level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(Village306.KEY).value(),chunk,0,new PiecesContainer(pieces));
diagnostics.put("houses",(int)houses);diagnostics.put("native-pieces",start.getPieces().size());diagnostics.put("columns",columns.size());
return new Village306.Plan(complete,cavity,Map.copyOf(diagnostics));
}
}
reject("no-fitting-site");return new Village306.Plan(null,null,Map.copyOf(diagnostics));
}
private String certify(VillageCavern306 cavity) {
var b=cavity.getBoundingBox(); int count=0,floors=0,roofs=0;
for(int z=b.minZ()+2;z<=b.maxZ();z+=6) for(int x=b.minX()+2;x<=b.maxX();x+=6) {
int roof=cavity.roofAt(x,z), floor=cavity.floorY(); if(roof<floor+2)continue; count++;
if(solid(x,floor-10,z))floors++;
boolean covered=false; for(int y=roof+1;y<=roof+14;y++) if(solid(x,y,z)&&solid(x,y+1,z)) {covered=true;break;}
if(covered)roofs++;
for(int y=floor-10;y<=roof+2;y+=2) if(!column(x,z).getBlock(y).getFluidState().isEmpty())return "fluid";
}
// The local floor can bridge small cracks; broad void-facing platforms are refused.
diagnostics.merge("best-floor-percent",count==0?0:floors*100/count,Math::max);
if(count==0 || floors<count*.8)return "thin-foundation";
if(roofs<count*.7)return "insufficient-cover";
return null;
}
private BlockPos access(StructureStart start,VillageCavern306 cave,List<BoundingBox> volumes,ChunkPos origin) {
int floor=cave.floorY(); var centre=start.getBoundingBox().getCenter();
var streets=start.getPieces().stream().filter(p->p instanceof PoolElementStructurePiece pool && pool.getElement().toString().contains("/streets/"))
.sorted(Comparator.comparingDouble((StructurePiece p)->p.getBoundingBox().getCenter().distSqr(centre)).reversed()).toList();
for(var street:streets) {
var at=street.getBoundingBox().getCenter();
for(var dir:net.minecraft.core.Direction.Plane.HORIZONTAL) {
List<BoundingBox> path=new ArrayList<>();
for(int step=0;step<=48;step+=3) {
int x=at.getX()+dir.getStepX()*step,z=at.getZ()+dir.getStepZ()*step;
if(Math.abs(x-origin.getMiddleBlockX())>116 || Math.abs(z-origin.getMiddleBlockZ())>116 || !root.contains(x,z))break;
var box=new BoundingBox(x-8,floor-5,z-8,x+8,floor+12,z+8);
if(host.intersectsExperimentalStructures(box.minX(),box.minY(),box.minZ(),box.maxX(),box.maxY(),box.maxZ()))break;
if(!solid(x,floor-5,z))break;
boolean fluid=false;for(int y=floor-5;y<=floor+6;y++)if(!column(x,z).getBlock(y).getFluidState().isEmpty())fluid=true;
if(fluid)break;
// Narrow connector volumes must retain the same eight-block shoulder convention.
path.add(new BoundingBox(x-8,floor-5,z-8,x+8,floor+12,z+8));
if(cave.roofAt(x,z)<0 && column(x,z).getBlock(floor).isAir() && column(x,z).getBlock(floor+2).isAir()) {
volumes.addAll(path);return new BlockPos(x,floor,z);
}
}
}
}
// Natural openings already intersecting a chamber are useful entrances too.
var b=cave.getBoundingBox();
for(int z=b.minZ();z<=b.maxZ();z+=4)for(int x=b.minX();x<=b.maxX();x+=4)
if(cave.projects(x,z)&&column(x,z).getBlock(floor).isAir()&&column(x,z).getBlock(floor+2).isAir()&&solid(x,floor-5,z))return new BlockPos(x,floor,z);
return null;
}
private StructureStart nativeStart(ChunkPos chunk,int floor) {
var registry=level.registryAccess().lookupOrThrow(Registries.STRUCTURE); var ops=RegistryOps.create(JsonOps.INSTANCE,level.registryAccess());
var original=registry.getValue(Identifier.withDefaultNamespace("village_plains"));
JsonObject json=Structure.DIRECT_CODEC.encodeStart(ops,original).getOrThrow().getAsJsonObject().deepCopy();
json.addProperty("start_pool","sanctuary:underground306/zombie_plains_starts");json.addProperty("size",6);json.addProperty("max_distance_from_center",80);
json.addProperty("terrain_adaptation","none");json.remove("project_start_to_heightmap");
var height=new JsonObject();height.addProperty("absolute",floor);json.add("start_height",height);
var structure=Structure.DIRECT_CODEC.parse(ops,json).getOrThrow();var random=host.randomState(root);
var proto=new ProtoChunk(chunk,UpgradeData.EMPTY,level,PalettedContainerFactory.create(level.registryAccess()),null);
return structure.generate(registry.getOrThrow(Village306.KEY),level.dimension(),level.registryAccess(),new FloorView(host,level,floor),host.getBiomeSource(),
random.createClimateSampler(SamplerContext.builder().enableCaches().build()),random,level.getStructureTemplateManager(),root.seed(),chunk,0,proto,b->!b.is(Biomes.DEEP_DARK));
}
private static final class FloorView extends ExpansionChunkGenerator {
final SanctuaryChunkGenerator host;final int floor;
FloorView(SanctuaryChunkGenerator host,ServerLevel level,int floor){super(host.source(),host.initialDiameter(),true);this.host=host;this.floor=floor;registries=level.registryAccess();}
@Override public int getBaseHeight(int x,int z,Heightmap.Types type,LevelHeightAccessor height,RandomState state){return floor;}
@Override public NoiseColumn getBaseColumn(int x,int z,LevelHeightAccessor height,RandomState state){return host.getBaseColumn(x,z,height,state);}
@Override public RandomState randomState(ExpansionIsland island){return host.randomState(island);}
}
private static long mix(long v){v=(v^(v>>>30))*0xbf58476d1ce4e5b9L;v=(v^(v>>>27))*0x94d049bb133111ebL;return v^(v>>>31);}
}
@@ -0,0 +1,11 @@
package fr.koka.sanctuary.worldgen.underground306;
import com.mojang.serialization.MapCodec;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureType;
public final class VillageStructure306 extends Structure {
public static final MapCodec<VillageStructure306> CODEC = simpleCodec(VillageStructure306::new);
public VillageStructure306(StructureSettings settings) { super(settings); }
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) { return Optional.empty(); }
@Override public StructureType<?> type() { return Village306.TYPE; }
}
@@ -210,6 +210,7 @@
"sanctuary.world_options.secrets23_hint": "Natural island, mineral secrets and abandoned railways. Vanilla structures remain possible. Expansions may contain oceans.",
"sanctuary.expansion.relief.ocean": "Oceanic continent",
"sanctuary.expansion.error.native_structure_unavailable": "No suitable structure found: expansion was not reserved.",
"structure.sanctuary.underground_abandoned_village": "Underground abandoned village",
"structure.sanctuary.mineral_sanctuary": "Mineral sanctuary",
"sanctuary.structures.census.unavailable": "Structure registry unavailable: coverage is unknown.",
"sanctuary.structures.census.summary": "Structures: %s starts, %s monster rooms, %s observed chunks.",
@@ -210,6 +210,7 @@
"sanctuary.world_options.secrets23_hint": "Île naturelle, secrets minéraux et voies abandonnées. Les structures vanilla restent possibles. Les expansions peuvent accueillir des océans.",
"sanctuary.expansion.relief.ocean": "Continent océanique",
"sanctuary.expansion.error.native_structure_unavailable": "Aucune structure adaptée trouvée : expansion non réservée.",
"structure.sanctuary.underground_abandoned_village": "Village abandonné souterrain",
"structure.sanctuary.mineral_sanctuary": "Sanctuaire minéral",
"sanctuary.structures.census.unavailable": "Registre des structures indisponible : couverture inconnue.",
"sanctuary.structures.census.summary": "Structures : %s départs, %s donjons, %s chunks observés.",
@@ -0,0 +1,63 @@
{
"ambient_light": 0.0,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
"block_search_extent": 8,
"offset": 2.0,
"sound": "minecraft:ambient.cave",
"tick_delay": 6000
}
},
"minecraft:audio/background_music": {
"creative": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.creative"
},
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.game"
}
},
"minecraft:gameplay/bed_rule": {
"can_set_spawn": "always",
"can_sleep": "when_dark",
"error_message": {
"translate": "block.minecraft.bed.no_sleep"
}
},
"minecraft:gameplay/nether_portal_spawns_piglin": true,
"minecraft:gameplay/respawn_anchor_works": false,
"minecraft:gameplay/straw_bed_rule": {
"can_set_spawn": "never",
"can_sleep": "when_dark",
"destroy_on_leave": true,
"error_message": {
"translate": "block.minecraft.bed.no_sleep"
}
},
"minecraft:visual/ambient_light_color": "#0a0a0a",
"minecraft:visual/cloud_color": "#ccffffff",
"minecraft:visual/cloud_height": 352.33,
"minecraft:visual/fog_color": "#c0d8ff",
"minecraft:visual/sky_color": "#78a7ff"
},
"coordinate_scale": 1.0,
"default_clock": "minecraft:overworld",
"has_ceiling": false,
"has_ender_dragon_fight": false,
"has_skylight": true,
"height": 640,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 640,
"min_y": 0,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": {
"type": "minecraft:uniform",
"max_inclusive": 7,
"min_inclusive": 0
},
"timelines": "#minecraft:in_overworld"
}
@@ -5,7 +5,7 @@
"legacy_random_source": true,
"material_rule": "sanctuary:unified_island",
"noise": {
"height": 384,
"height": 640,
"min_y": 0
},
"noise_router": {
@@ -5,7 +5,7 @@
"legacy_random_source": true,
"material_rule": "sanctuary:unified_island",
"noise": {
"height": 384,
"height": 640,
"min_y": 0
},
"noise_router": {
@@ -5,7 +5,7 @@
"legacy_random_source": true,
"material_rule": "sanctuary:unified_island",
"noise": {
"height": 384,
"height": 640,
"min_y": 0
},
"noise_router": {
@@ -0,0 +1,7 @@
{
"type": "sanctuary:underground_site_v306",
"biomes": "#minecraft:is_overworld",
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -0,0 +1,41 @@
{
"elements": [
{
"element": {
"element_type": "minecraft:legacy_single_pool_element",
"location": "minecraft:village/plains/zombie/town_centers/plains_fountain_01",
"processors": "minecraft:zombie_plains",
"projection": "rigid"
},
"weight": 1
},
{
"element": {
"element_type": "minecraft:legacy_single_pool_element",
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_1",
"processors": "minecraft:zombie_plains",
"projection": "rigid"
},
"weight": 1
},
{
"element": {
"element_type": "minecraft:legacy_single_pool_element",
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_2",
"processors": "minecraft:zombie_plains",
"projection": "rigid"
},
"weight": 1
},
{
"element": {
"element_type": "minecraft:legacy_single_pool_element",
"location": "minecraft:village/plains/zombie/town_centers/plains_meeting_point_3",
"processors": "minecraft:zombie_plains",
"projection": "rigid"
},
"weight": 1
}
],
"fallback": "minecraft:empty"
}
@@ -1,7 +1,7 @@
{
"dimensions": {
"minecraft:overworld": {
"type": "sanctuary:sanctuary_384",
"type": "sanctuary:sanctuary_640",
"generator": {
"type": "sanctuary:island_v24",
"biome_source": {
@@ -33,7 +33,9 @@
"StructureCensusMonsterRoomMixin",
"RuinedPortal23Accessor",
"IglooHeight23Mixin",
"ScatteredHeight23Accessor"
"ScatteredHeight23Accessor",
"UndergroundHeight306Mixin",
"UndergroundStart306Mixin"
],
"client": [
"client.SanctuarySkyMixin",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.30.3"
version = "0.1.0-alpha.30.6"
pack-format = "packwiz:1.1.0"
[index]
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Freeze a rendered native atlas with raw measurements, source revision and build hash."""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
def sha(data):
return hashlib.sha256(data).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--raw", type=Path, required=True)
parser.add_argument("--rendered", type=Path, required=True)
parser.add_argument("--jar", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--extra", type=Path, action="append", default=[])
args = parser.parse_args()
if args.output.exists():
raise SystemExit("Refusing to overwrite an existing immutable atlas")
status = subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True)
if status.strip():
raise SystemExit("Commit atlas code and documentation before freezing the archive")
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
jar_bytes = args.jar.read_bytes()
with zipfile.ZipFile(args.jar) as jar:
if jar.testzip() is not None:
raise SystemExit("Invalid built JAR")
version = json.loads(jar.read("fabric.mod.json"))["version"]
files = {}
cases = []
for source in sorted(args.raw.glob("terrain-*.json")):
raw = source.read_bytes()
data = json.loads(raw)
if data["version"] != version:
raise SystemExit(f"Stale native export: {source}")
rendered = args.rendered / source.stem
provenance = json.loads((rendered / "provenance.json").read_text())
if provenance["source_sha256"] != sha(raw):
raise SystemExit(f"Rendered atlas differs from source: {source}")
if provenance["script_sha256"] != sha((ROOT / "scripts/terrain_atlas.py").read_bytes()):
raise SystemExit("Rendering script changed after rendering")
files["raw/" + source.name] = raw
for name in ("maps.png", "maps.pdf", "sections.png", "sections.pdf", "summary.json", "provenance.json"):
files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
for name in ("upper-terrain.png", "upper-terrain.pdf"):
if (rendered / name).is_file(): files[f"figures/{source.stem}/{name}"] = (rendered / name).read_bytes()
cases.append({"seed": data["seed"], "diameter": data["diameter"], "directory": source.stem})
identities = {(c["seed"], c["diameter"]) for c in cases}
if not cases or len(identities) != len(cases):
raise SystemExit("Empty atlas or duplicate seed/size")
for name in ("scripts/terrain_atlas.py", "scripts/terrain-atlas-requirements.txt",
"scripts/archive_terrain_atlas.py", "docs/terrain-atlas.md",
"scripts/structure_atlas.py", f"docs/generation-alpha{version.split('alpha.')[-1]}.md", f"docs/testing-alpha{version.split('alpha.')[-1]}.md"):
files[name] = (ROOT / name).read_bytes()
for path in args.extra:
key = "structures/" + path.name
if key in files: raise SystemExit("Duplicate extra atlas file")
files[key] = path.read_bytes()
rows = ["# Sanctuary — atlas " + version, "", f"Source : `{commit}`.", "",
"Mesures natives avant décoration ; voir docs/terrain-atlas.md pour les échelles et limites.", "",
"| Diamètre | Graine | Cartes | Coupes |", "|---|---|---|---|"]
for c in cases:
base = "figures/" + c["directory"]
rows.append(f"| {c['diameter']} | {c['seed']} | [PNG]({base}/maps.png) / [PDF]({base}/maps.pdf) | [PNG]({base}/sections.png) / [PDF]({base}/sections.pdf) |")
rows += ["", "Reproduction des figures dans un environnement Python isolé :", "", "```sh",
"python -m pip install -r scripts/terrain-atlas-requirements.txt",
"python scripts/terrain_atlas.py --output rendered raw/terrain-*.json", "```", ""]
files["README.md"] = "\n".join(rows).encode()
files["manifest.json"] = (json.dumps({"version": version, "source_commit": commit,
"jar": args.jar.name, "jar_sha256": sha(jar_bytes), "cases": cases,
"files_sha256": {name: sha(content) for name, content in sorted(files.items())}}, indent=2) + "\n").encode()
args.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(args.output, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, content in sorted(files.items()):
info = zipfile.ZipInfo(name, date_time=(2000, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, content)
with zipfile.ZipFile(args.output) as archive:
assert archive.testzip() is None
for name, content in files.items():
assert archive.read(name) == content
print(json.dumps({"path": str(args.output), "cases": len(cases), "source_commit": commit,
"sha256": sha(args.output.read_bytes()), "bytes": args.output.stat().st_size}))
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Plot observed village blocks and planned surface rails against the native terrain atlas."""
import argparse,json,hashlib
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
def render(source, terrain, output):
data=json.loads(source.read_text()); native=json.loads(terrain.read_text())
if not data.get('passed') or data['seed']!=str(native['seed']) or data['diameter']!=native['diameter']:
raise ValueError('Mismatched or failed native witnesses')
output.mkdir(parents=True,exist_ok=True)
name=f"structures-{data['diameter']}-{data['seed']}"
columns=np.asarray(native['variants']['current306']['columns'])
xs,zs=np.unique(columns[:,0]),np.unique(columns[:,1]);grid=columns.reshape(len(zs),len(xs),5)
extent=(xs[0]-6,xs[-1]+6,zs[-1]+6,zs[0]-6)
fig,ax=plt.subplots(figsize=(10,10),layout='constrained')
img=ax.imshow(np.ma.masked_where(grid[:,:,2]==0,grid[:,:,2]),extent=extent,cmap='terrain',vmin=0,vmax=400,interpolation='nearest')
freight=[l for l in data['rail_links'] if l['kind']=='FREIGHT']
rail_area=len({(c['x'],c['z']) for c in data['rail_cells'] if c['rail']!='NONE'})
ballast=len({(c['x'],c['z']) for c in data['rail_cells']})
land=int(np.count_nonzero(grid[:,:,2]))*native['map_step']**2
upper=int(np.count_nonzero(grid[:,:,2]>=180))*native['map_step']**2
for link in data['rail_links']:
points=link['points'];ax.plot([p['x'] for p in points],[p['z'] for p in points],color='#601d15' if link['kind']=='FREIGHT' else '#404040',lw=1.5 if link['kind']=='FREIGHT' else .9)
ax.scatter([0],[0],marker='+',s=70,c='black',label='Origine naturelle protégée')
ax.plot([],[],color='#601d15',label='Voies ferrées prévues (lacunes réparables)')
ax.plot([],[],color='#404040',label='Chemins prévus')
ax.set(xlabel='X (blocs)',ylabel='Z (blocs) · nord en haut',aspect='equal')
ax.set_title(f"Sanctuary {data['diameter']} · graine {data['seed']}\nRéseau de surface sur le relief natif 30.6")
ax.legend(fontsize=9,loc='upper left');fig.colorbar(img,ax=ax,shrink=.72,label='Altitude du terrain (Y)')
fig.supxlabel(f"{rail_area} colonnes de voie · {ballast} colonnes de sol aménagé (chemins inclus)\n"
f"{100*ballast/land:.2f} % de lemprise terrestre projetée estimée ; {100*ballast/upper:.2f} % des surfaces ≥ Y180.\n"
"Relief échantillonné tous les 12 blocs ; tracés issus du plan, avant placement complet des rails.",fontsize=9)
for ext in ['png','pdf']:fig.savefig(output/f'{name}-reseau.{ext}',dpi=160)
plt.close(fig)
if 'floor_cut' in data:
floor=data['floor_cut']; ids=np.array(floor['blocks']); y=floor['y']
def category(s):
if s in ('minecraft:air','minecraft:cave_air','minecraft:void_air'):return 0
if s in ('minecraft:stone','minecraft:dirt','minecraft:granite','minecraft:andesite','minecraft:diorite','minecraft:deepslate','minecraft:tuff'):return 1
if 'water' in s:return 4
if 'cobweb' in s:return 3
return 2
pixels=np.vectorize(category)(ids)
fig,axes=plt.subplots(1,3,figsize=(18,7),layout='constrained')
palette=ListedColormap(['#eef1f0','#666d70','#b98651','#976da6','#489dc8'])
axes[0].imshow(pixels,extent=(floor['min_x']-.5,floor['min_x']+ids.shape[1]-.5,floor['min_z']+ids.shape[0]-.5,floor['min_z']-.5),cmap=palette,vmin=0,vmax=4,interpolation='nearest',aspect='equal')
axes[0].set_title(f'Coupe horizontale Y{y}\nBrun : constructions / décor ; violet : toiles')
axes[0].set(xlabel='X (blocs)',ylabel='Z (blocs)')
for ax,cut in zip(axes[1:],data['native_cuts']):
blocks=np.asarray(cut['blocks']);step=cut['step']
ax.imshow(blocks,extent=(cut['min_x']-1,cut['min_x']+blocks.shape[1]*step-1,-1,301),cmap=ListedColormap(['#eef1f0','#666d70','#489dc8','#ea8542']),vmin=0,vmax=3,interpolation='nearest',aspect='equal')
ax.axhline(data['floor'],color='#b98651',lw=.8,ls='--');ax.set(xlabel='X (blocs)',ylabel='Y (blocs)');ax.set_title(f"Coupe verticale Z{cut['z']}\nTrait : sol du village")
fig.suptitle(f"Village abandonné · diamètre {data['diameter']} · graine {data['seed']}\nBlocs réellement générés, après structures et décoration",fontsize=15)
fig.supxlabel(f"{data['diagnostics']['houses']} maisons natives · {data['chests']} coffres · {data['zombie_villagers']} zombie-villageois\n"
"Échantillonnage horizontal : 1 bloc ; vertical : 2 blocs. Aucune interpolation daffichage.",fontsize=10)
for ext in ['png','pdf']:fig.savefig(output/f'{name}-village.{ext}',dpi=160)
plt.close(fig)
result={'seed':data['seed'],'diameter':data['diameter'],'rail_columns':rail_area,'prepared_columns_including_paths':ballast,'estimated_land_area':land,
'ratio_percent':100*ballast/land,'ratio_upper_percent':100*ballast/upper,'structure_source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),
'terrain_source_sha256':hashlib.sha256(terrain.read_bytes()).hexdigest()}
(output/f'{name}-summary.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result))
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__);p.add_argument('source',type=Path);p.add_argument('terrain',type=Path);p.add_argument('--output',type=Path,required=True);a=p.parse_args();render(a.source,a.terrain,a.output)
+2
View File
@@ -0,0 +1,2 @@
matplotlib==3.11.2
numpy==2.5.3
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Render TerrainAtlas305GameTests native exports. No synthesized terrain or image smoothing.
python scripts/terrain_atlas.py --output build/atlas305 <native-json> [<native-json> ...]
See docs/terrain-atlas.md for acquisition, provenance and sampling limits.
"""
import argparse
import hashlib
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
import numpy as np
LABELS = {
"plateaus27": "Plateaux 24 / 27",
"global301": "Bruit 3D global · 30.1",
"stretch304": "Étirement vertical · 30.4",
"current305": "Plateaux + déformation 3D · 30.5",
"current306": "Relief légèrement renforcé · 30.6",
}
def save(fig, directory, name):
fig.savefig(directory / f"{name}.png", dpi=150, facecolor="white")
fig.savefig(directory / f"{name}.pdf", facecolor="white")
plt.close(fig)
def render(source, output):
raw = source.read_bytes()
data = json.loads(raw)
if data["schema"] != 1 or len(data["variants"]) != 4 or any(k not in LABELS for k in data["variants"]):
raise ValueError(f"Unsupported atlas schema/variants: {source}")
labels = {key: LABELS[key] for key in data["variants"]}
target = output / source.stem
target.mkdir(parents=True, exist_ok=True)
title = f"Sanctuary · diamètre {data['diameter']} · graine {data['seed']}"
provenance = (f"Champ natif Minecraft {data['minecraft']} · {data['version']} · {data['revision']}\n"
"Avant eau, matériaux de surface, végétation et structures. Le plafond de construction reste Y640.")
variants = list(data["variants"].values())
maps = []
for variant in variants:
columns = np.asarray(variant["columns"], dtype=float)
xs, zs = np.unique(columns[:, 0]), np.unique(columns[:, 1])
if columns.shape[0] != len(xs) * len(zs):
raise ValueError("Incomplete native map grid")
maps.append(columns.reshape(len(zs), len(xs), 5))
step = data["map_step"]
extent = (xs[0] - step / 2, xs[-1] + step / 2, zs[-1] + step / 2, zs[0] - step / 2)
fig, axes = plt.subplots(3, 4, figsize=(18, 14), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nRelief et épaisseur de roche", fontsize=18)
metrics = [(2, "Altitude du sommet (Y)", "terrain", 0, data["domain_y"][1]),
(3, "Épaisseur solide cumulée (blocs)", "viridis", 0, 300),
(4, "Roche continue sous le sommet (blocs)", "magma", 0, 100)]
for row, (index, label, cmap, vmin, vmax) in enumerate(metrics):
for col, (key, grid) in enumerate(zip(labels, maps)):
ax = axes[row, col]
values = np.ma.masked_where(grid[:, :, 2] == 0, grid[:, :, index])
image = ax.imshow(values, extent=extent, origin="upper", interpolation="nearest",
cmap=cmap, vmin=vmin, vmax=vmax, aspect="equal")
ax.set_facecolor("#e8edf0")
ax.set_xlabel("X (blocs)")
if col == 0:
ax.set_ylabel("Z (blocs) · nord en haut")
if row == 0:
ax.set_title(labels[key], fontsize=11)
ax.plot(0, 0, "+", color="black", ms=8, label="Origine")
fig.colorbar(image, ax=axes[row, :], shrink=.85, label=label, extend="max")
fig.supxlabel(f"Pas horizontal : {step} blocs · vertical : {data['vertical_step']} blocs · gris = aucun solide échantillonné.\n" + provenance, fontsize=9)
save(fig, target, "maps")
section_count = len(variants[0]["sections"])
fig, axes = plt.subplots(4, section_count, figsize=(20, 12), layout="constrained", sharex=True, sharey=True)
fig.suptitle(title + "\nCoupes de densité · proportions spatiales conservées", fontsize=18)
for row, (key, variant) in enumerate(zip(labels, variants)):
for col, section in enumerate(variant["sections"]):
ax = axes[row, col]
density = np.asarray(section["density"], dtype=float) / data["density_scale"]
dy = section["step"]
x = section["min_x"] + np.arange(density.shape[1]) * dy
y = section["max_y"] - np.arange(density.shape[0]) * dy
image = ax.imshow(density, extent=(x[0]-dy/2, x[-1]+dy/2, y[-1]-dy/2, y[0]+dy/2),
interpolation="nearest", cmap="RdBu_r", norm=TwoSlopeNorm(0, -.3, .3), aspect="equal")
ax.contour(x, y, density, levels=[0], colors="#262626", linewidths=.35)
if row == 0:
ax.set_title(f"Plan vertical Z = {section['z']}")
if col == 0:
ax.set_ylabel(labels[key] + "\nY (blocs)", fontsize=10)
ax.set_xlabel("X (blocs)")
ax.set_ylim(0, data["domain_y"][1] + 1)
fig.colorbar(image, ax=axes, shrink=.6, label="Densité sans unité · bleu : vide (≤ 0) · rouge : roche (> 0)", extend="both")
fig.supxlabel("Coupes échantillonnées tous les 2 blocs, sans interpolation daffichage ; isosurface zéro en noir.\n" + provenance, fontsize=9)
save(fig, target, "sections")
upper = data["variants"].get("current306", {}).get("upper_sections", [])
if upper:
fig, axes = plt.subplots(1, len(upper), figsize=(5*len(upper), 6), layout="constrained", squeeze=False)
fig.suptitle(title + "\nMorceaux de terrain supérieur · densité native", fontsize=15)
for ax, cut in zip(axes[0], upper):
values = np.array(cut["solid"])
dy=cut["step"]
ax.imshow(values, cmap="Greys", vmin=0, vmax=1, interpolation="nearest", aspect="equal",
extent=(cut["min_x"]-1,cut["min_x"]+values.shape[1]*dy-1,cut["max_y"]-values.shape[0]*dy+1,cut["max_y"]+1))
ax.set_title(f"Z = {cut['z']}"); ax.set_xlabel("X");ax.set_ylabel("Y")
fig.supxlabel("Noir : roche · blanc : air. Aucune interpolation daffichage. Avant végétation et structures.",fontsize=9)
save(fig,target,"upper-terrain")
summary = {"seed": data["seed"], "diameter": data["diameter"], "variants": {}}
base = maps[0][:, :, 2]
common = np.logical_and.reduce([m[:, :, 2] >= 200 for m in maps])
for key, grid in zip(labels, maps):
land = grid[:, :, 2] > 0
uplands = base >= 200
summary["variants"][key] = {
"sampled_land_columns": int(land.sum()),
"max_sampled_y": float(grid[:, :, 2].max()),
"original_uplands_within_2_blocks_fraction": float((np.abs(grid[:, :, 2]-base)[uplands] <= 2).mean()),
"common_upper_columns": int(common.sum()),
"median_roof_thickness_common_upper_columns": float(np.median(grid[:, :, 4][common])),
"roof_at_most_8_blocks_common_upper_fraction": float((grid[:, :, 4][common] <= 8).mean()),
}
(target / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
(target / "provenance.json").write_text(json.dumps({
"source": source.name, "source_sha256": hashlib.sha256(raw).hexdigest(),
"script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"matplotlib": matplotlib.__version__, "numpy": np.__version__,
"version": data["version"], "revision": data["revision"], "process_id": data["process_id"],
"scope": data["scope"], "map_step": step, "vertical_step": data["vertical_step"],
"density_quantization": 1 / data["density_scale"], "density_plot_saturation": [-.3, .3],
}, indent=2) + "\n")
print(json.dumps(summary))
return summary
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("sources", nargs="+", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
summaries = [render(source, args.output) for source in args.sources]
(args.output / "summary.json").write_text(json.dumps(summaries, indent=2) + "\n")
if __name__ == "__main__":
main()