Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7e0c6717 | ||
|
|
fbd870a4e3 | ||
|
|
16e480bfd0 | ||
|
|
7f81bf35a0 | ||
|
|
e400f1e2c1 | ||
|
|
2dc70fe905 | ||
|
|
a49dfa5337 |
@@ -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 l’arè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.
|
||||
|
||||
@@ -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 d’une cavité locale adaptée ; les mines sont réservées à
|
||||
l’avance et les donjons natifs disposent de davantage de tentatives. Les voies
|
||||
abandonnées s’allongent et se ramifient entre les plateaux.
|
||||
Deux nouveaux morceaux de terrain naturel sur Petit/Moyen, trois sur Grand,
|
||||
occupent le ciel vers Y300–400. 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 [l’atlas 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 n’est 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), [l’outil d’atlas](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).
|
||||
|
||||
@@ -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 d’eau 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 l’espace constructible jusqu’à
|
||||
Y639. Stabiliser un seul relief avant d’ajouter des personnalisations.
|
||||
L’arè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 d’eau,
|
||||
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 [l’atlas](terrain-atlas.md).
|
||||
|
||||
@@ -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 Y354–359, 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 d’appui et d’espace pour les arbres.
|
||||
|
||||
L’hydrologie cherche un cours d’eau supplémentaire sur les hauts plateaux,
|
||||
puis conserve la recherche d’une 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. L’arène du destin est
|
||||
uniquement une idée pour un futur défi en altitude : aucune plateforme,
|
||||
panneau ni arène n’est 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 n’est 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).
|
||||
@@ -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 n’est 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. L’arrivé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 l’union entre terrain étiré et terrain bas
|
||||
sont retirés du profil courant. Les sommets retrouvent la hauteur produite par
|
||||
le champ natif ; **Y360 n’est 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 d’altitude 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.
|
||||
|
||||
L’ancienne 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 n’est réalisée : **créer un nouveau
|
||||
monde**. Cette révision des profils d’essai n’offre pas de continuité du terrain
|
||||
pour les chunks futurs d’une ancienne sauvegarde.
|
||||
|
||||
Les îles supérieures, expansions choisies à partir de la graine et histoires
|
||||
d’expédition seront définies plus tard. Aucun récit, événement ou continent
|
||||
supplémentaire n’est injecté dans cette livraison.
|
||||
|
||||
Résultats et limites : [vérifications alpha.30.5](testing-alpha30.5.md).
|
||||
@@ -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 l’accord 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
|
||||
|
||||
L’amplitude 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 n’y a ni sommet imposé ni ajout de bruit de détail. L’atlas 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 d’air dessous. Le filtrage des
|
||||
petits fragments s’applique également à ces hauteurs. La surface projetée
|
||||
reste minoritaire ; aucun semis de blocs flottants n’est ajouté.
|
||||
|
||||
L’idée d’un lieu exceptionnel vers **Y600** est conservée pour plus tard.
|
||||
Aucune structure, plateforme ou arène n’y 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 n’est remplacé, aucun graphe n’est 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 l’emplacement.
|
||||
- Le sous-sol doit offrir une majorité d’appuis 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 n’est 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 d’appuis et d’altitude existants. Le plan
|
||||
accepté est celui effectivement placé et localisé. Un échec reste explicite
|
||||
au journal ; les graines testées ne prouvent pas l’ensemble des graines possibles.
|
||||
Les mines restent indépendantes du commutateur expérimental ; l’option 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 d’un donjon sont nettoyées dans son volume, sans
|
||||
modifier les plantes valides ni produire de drops. Cela s’applique aussi
|
||||
aux expansions du générateur courant ; aucune expansion n’est 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 n’est nécessaire pour conserver une voie.
|
||||
L’arrivée naturelle reste protégée sur 48 blocs autour de l’origine.
|
||||
|
||||
Les **6–8 % évoqués sont un repère visuel, pas un quota imposé**. L’atlas des
|
||||
structures mesure les colonnes aménagées et les superpose au relief. Les voies
|
||||
s’arrêtent lorsqu’un 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 d’arrê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 s’arrê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 d’atlas](terrain-atlas.md).
|
||||
+139
@@ -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 Y300–400.
|
||||
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
|
||||
l’ultime 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 l’atlas.
|
||||
L’assemblage 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 n’est 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 d’une déformation 3D modérée. Aucun sommet Y360 n’est 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 n’est 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 d’amorç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 d’instance 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)
|
||||
|
||||
@@ -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 n’est 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 d’en faire une archive. Aucune sauvegarde personnelle n’est utilisée.
|
||||
|
||||
## Lecture des planches
|
||||
|
||||
- Cartes : nord en haut, X et Z en blocs, rapport d’aspect réel. Les quatre
|
||||
profils partagent les mêmes échelles de couleur. La palette d’altitude ne
|
||||
représente pas les blocs ou les biomes ; du blanc n’implique pas de neige.
|
||||
- Altitude : dernier solide rencontré, recherche verticale par pas de 2 blocs.
|
||||
Une cellule grise signifie qu’aucun solide n’a é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 n’est ni une densité physique, ni un volume exact de matériaux minables.
|
||||
- Roche continue : épaisseur depuis le sommet jusqu’au 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 d’un bloc dans les tests complets.
|
||||
|
||||
L’eau, 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é d’une 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.
|
||||
|
||||
L’archive 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 n’intervient.
|
||||
|
||||
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
|
||||
|
||||
L’export 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 n’est
|
||||
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
|
||||
```
|
||||
@@ -95,3 +95,11 @@ Essais exécutés sur Mac ; aucun essai natif Windows par l’agent. Les variati
|
||||
de relief exigent un nouveau monde ; aucun monde personnel n’est régénéré.
|
||||
Le filtrage local de connexité n’est 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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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. L’atlas 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 n’est
|
||||
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 s’agit pas
|
||||
d’un inventaire de chaque bloc. Les coupes sont tracées avec des proportions
|
||||
réelles et des échelles communes, sans interpolation d’affichage.
|
||||
|
||||
L’épaisseur du toit ne s’amé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
|
||||
l’atlas correspondent donc aux archives effectivement produites précédemment.
|
||||
Reçu local : `build/alpha305-historical-equivalence.json`.
|
||||
|
||||
L’archive 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 d’atlas](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
|
||||
d’un 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 d’eau 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 d’expansion 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 n’est retenue par le plan sur
|
||||
ce témoin : aucune cascade fluviale n’est 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 d’amorç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 l’objet 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 l’eau, 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 d’un bloc, pas sur chaque bloc du monde.
|
||||
Aucun test de rendu client ou shader nouveau n’est revendiqué dans cette passe.
|
||||
Les contrôles fonctionnels du Blocodex sont côté serveur ; son interface n’a
|
||||
pas été modifiée. Aucun essai Windows, aucune activation d’expansion ni
|
||||
modification d’une sauvegarde personnelle. Créer un nouveau monde.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Vérifications alpha.30.6
|
||||
|
||||
Essais natifs sur **Mac**, Minecraft 26.3-pre-2, Java 25. Aucun essai Windows
|
||||
natif n’est revendiqué. Un **nouveau monde** est nécessaire.
|
||||
|
||||
## Atlas final
|
||||
|
||||
Neuf couples : Petit 512 / Moyen 724 / Grand 1024 × graines 0, 42 et
|
||||
-7228211907433324401. Le serveur d’export plat passe ses deux tests ;
|
||||
l’acquisition 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**.
|
||||
L’augmentation du domaine d’affichage à Y420 ajoute seulement de l’air 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 d’expansion ; ses corridors et leurs éléments sont inspectés.
|
||||
93 salles vanilla `monster_room` ont été observées dans l’ensemble de cette
|
||||
passe : ce n’est pas le nombre total de donjons de l’île ni une garantie par graine.
|
||||
|
||||
Ce témoin complet précède l’ultime sculpture des morceaux de terrain aérien.
|
||||
La génération sous Y300 n’a 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 d’un 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
|
||||
n’est pas une mesure du temps d’ouverture d’un 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 l’emprise terrestre projetée estimée (0,96 % au-dessus de Y180),
|
||||
et non les 6–8 % é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
|
||||
|
||||
L’assemblage 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.
|
||||
L’assemblage 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`.
|
||||
L’archive fige les mesures, les images et l’empreinte du JAR de cette livraison.
|
||||
Les pourcentages de voies sont des mesures du plan ; aucun trajet en minecart
|
||||
sur l’intégralité du réseau n’est 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 n’est implémentée.
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.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
|
||||
|
||||
@@ -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') }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+62
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
|
||||
+7
@@ -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");
|
||||
}
|
||||
|
||||
+17
@@ -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());
|
||||
|
||||
+6
-3
@@ -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)),
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+6
-6
@@ -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();
|
||||
|
||||
+136
@@ -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());
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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()
|
||||
|
||||
+10
-2
@@ -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));
|
||||
}
|
||||
}
|
||||
+8
@@ -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;
|
||||
}
|
||||
|
||||
+26
-5
@@ -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() {
|
||||
|
||||
+22
-1
@@ -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;
|
||||
|
||||
+75
-5
@@ -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;
|
||||
|
||||
+42
@@ -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() {}
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -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);}
|
||||
}
|
||||
+11
@@ -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"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:unified_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"height": 640,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:unified_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"height": 640,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:unified_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"height": 640,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "sanctuary:underground_site_v306",
|
||||
"biomes": "#minecraft:is_overworld",
|
||||
"step": "top_layer_modification",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+41
@@ -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
-1
@@ -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
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
@@ -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 l’emprise 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 d’affichage.",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)
|
||||
@@ -0,0 +1,2 @@
|
||||
matplotlib==3.11.2
|
||||
numpy==2.5.3
|
||||
@@ -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 d’affichage ; 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 d’affichage. 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()
|
||||
Reference in New Issue
Block a user