Restore native plateaus and archive terrain comparisons for alpha.30.5
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -21,6 +21,18 @@ 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
|
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
|
||||||
[chantier et ses limites](docs/generation-alpha27.md).
|
[chantier et ses limites](docs/generation-alpha27.md).
|
||||||
|
|
||||||
|
## 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
|
## Alpha.30.4 — massifs et ciel constructible
|
||||||
|
|
||||||
Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des
|
Les plateaux bas sont conservés ; les reliefs hauts se raccordent par des
|
||||||
|
|||||||
@@ -1037,3 +1037,13 @@ courbes sur les replats supérieurs et libérer de l’espace constructible jusq
|
|||||||
Y639. Stabiliser un seul relief avant d’ajouter des personnalisations.
|
Y639. Stabiliser un seul relief avant d’ajouter des personnalisations.
|
||||||
L’arène du destin reste une idée, sans structure ajoutée.
|
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).
|
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.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,75 @@
|
|||||||
|
# Atlas scientifique du terrain
|
||||||
|
|
||||||
|
## 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/atlas305 \
|
||||||
|
mods/sanctuary/build/run/gameTest/diagnostics/atlas305/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..382.
|
||||||
|
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.
|
||||||
@@ -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.
|
||||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
|||||||
loom_version=1.17.20
|
loom_version=1.17.20
|
||||||
fabric_api_version=0.160.0+26.3
|
fabric_api_version=0.160.0+26.3
|
||||||
|
|
||||||
mod_version=0.1.0-alpha.30.4
|
mod_version=0.1.0-alpha.30.5
|
||||||
pack_version=0.1.0-alpha.30.4
|
pack_version=0.1.0-alpha.30.5
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ tasks.register('cleanGameTestWorld', Delete) {
|
|||||||
}
|
}
|
||||||
tasks.named('runGameTest') {
|
tasks.named('runGameTest') {
|
||||||
dependsOn('cleanGameTestWorld')
|
dependsOn('cleanGameTestWorld')
|
||||||
|
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.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
|
||||||
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
|
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
|
||||||
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
||||||
@@ -84,6 +87,8 @@ tasks.named('runGameTest') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('processGametestResources') {
|
tasks.named('processGametestResources') {
|
||||||
|
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('atlasOnly', atlasOnly)
|
||||||
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('benchmark', benchmark)
|
inputs.property('benchmark', benchmark)
|
||||||
def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true)
|
def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true)
|
||||||
@@ -127,10 +132,11 @@ tasks.named('processGametestResources') {
|
|||||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('voidReuse', voidReuse)
|
inputs.property('voidReuse', voidReuse)
|
||||||
filesMatching('fabric.mod.json') {
|
filesMatching('fabric.mod.json') {
|
||||||
expand(gametest_entrypoint: 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: 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'
|
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||||
|
if (atlasOnly) filter { line -> line.replaceAll(/("fabric-gametest": \["[^"]+")[^\]]*/, '$1') }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -16,13 +16,13 @@ import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
|||||||
/** Compares registered native fields; does not place blocks or load additional chunks. */
|
/** Compares registered native fields; does not place blocks or load additional chunks. */
|
||||||
public final class Terraces29WorldGameTests {
|
public final class Terraces29WorldGameTests {
|
||||||
@GameTest(maxTicks = 2400)
|
@GameTest(maxTicks = 2400)
|
||||||
public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception {
|
public void broadProvincesPreservePlateausWithoutStretchingCaves(GameTestHelper helper) throws Exception {
|
||||||
var level = helper.getLevel();
|
var level = helper.getLevel();
|
||||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||||
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
||||||
helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.4 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 random = generator.starterContext().random();
|
||||||
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
|
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
|
||||||
var context = SamplerContext.builder().enableCaches().build();
|
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");
|
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_lower_density_samples", lowerSamples);
|
||||||
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
|
||||||
var sections = new LinkedHashMap<String, Object>();
|
var sections = new LinkedHashMap<String, Object>();
|
||||||
@@ -123,7 +123,7 @@ public final class Terraces29WorldGameTests {
|
|||||||
}
|
}
|
||||||
report.put("sizes",sizes);
|
report.put("sizes",sizes);
|
||||||
report.put("passed", true);
|
report.put("passed", true);
|
||||||
Path path = Path.of("diagnostics/alpha304", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
Path path = Path.of("diagnostics/alpha305", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||||
Files.createDirectories(path.getParent());
|
Files.createDirectories(path.getParent());
|
||||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
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("stretch304", new Historical304(field));
|
||||||
|
functions.put("current305", 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<Object>();
|
||||||
|
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 = 382; 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 = 382; 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", 382, "step", 2, "density", rows));
|
||||||
|
}
|
||||||
|
variants.put(entry.getKey(), Map.of("columns", columns, "sections", sections));
|
||||||
|
}
|
||||||
|
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, 383)); 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/atlas305", "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(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -46,6 +46,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
|||||||
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
||||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||||
|
if (Boolean.getBoolean("sanctuary.test.atlasOnly")) return net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT_ALL_DIMENSIONS;
|
||||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||||
int generation = Integer.getInteger("sanctuary.test.generation", 24);
|
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");
|
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,
|
private static net.minecraft.world.level.levelgen.WorldDimensions sanctuary$customizeSize(WorldPreset preset,
|
||||||
net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) {
|
net.minecraft.world.level.LevelSettings settings, net.minecraft.server.WorldLoader.DataLoadContext context) {
|
||||||
var dimensions = preset.createWorldDimensions();
|
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();
|
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
|
||||||
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
|
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
|
||||||
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
|
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
|
||||||
|
|||||||
+1
-2
@@ -52,8 +52,7 @@ public final class PopulationHydrologyRuntime {
|
|||||||
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
|
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
|
||||||
});
|
});
|
||||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||||
var water = (current30(generator) ? PopulationHydrology.withHighPlateaus304(seed, memo)
|
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||||
: PopulationHydrology.create(seed, memo)).admitted();
|
|
||||||
if (current30(generator)) {
|
if (current30(generator)) {
|
||||||
water = BankOutlets30.openBanks(seed, water, memo, 6);
|
water = BankOutlets30.openBanks(seed, water, memo, 6);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -49,6 +49,13 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||||
|
return compileSampler(context, false);
|
||||||
|
}
|
||||||
|
/** Exact rejected30.4 field retained for the reproducible scientific comparison. */
|
||||||
|
public DensitySampler compileHistorical304(CompileContext context) {
|
||||||
|
return compileSampler(context, true);
|
||||||
|
}
|
||||||
|
private DensitySampler compileSampler(CompileContext context, boolean historical304) {
|
||||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||||
@@ -57,7 +64,10 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
|||||||
var ridges = geology28 ? ridges28.compileSampler(context) : null;
|
var ridges = geology28 ? ridges28.compileSampler(context) : null;
|
||||||
var erosion = geology28 ? erosion28.compileSampler(context) : null;
|
var erosion = geology28 ? erosion28.compileSampler(context) : null;
|
||||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||||
var balanced = balanced303 ? new HighPlateaus304(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>();
|
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||||
DensitySampler field = new DensitySampler() {
|
DensitySampler field = new DensitySampler() {
|
||||||
private float terrainAt(SamplerContext sample, double x, double y, double z) {
|
private float terrainAt(SamplerContext sample, double x, double y, double z) {
|
||||||
@@ -83,7 +93,8 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
|||||||
double province = balanced != null ? balanced.province(x, z) : 0;
|
double province = balanced != null ? balanced.province(x, z) : 0;
|
||||||
double mappedY = balanced != null && province > 0 ? HighPlateaus304.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;
|
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
|
||||||
if (balanced != null) weight *= province;
|
if (plateaus != null) weight *= .8 * plateaus.province(x, z);
|
||||||
|
else if (balanced != null) weight *= province;
|
||||||
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
|
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
|
||||||
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
|
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
|
||||||
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
|
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "0.1.0-alpha.30.4"
|
version = "0.1.0-alpha.30.5"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/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)
|
||||||
|
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()
|
||||||
|
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",
|
||||||
|
"docs/generation-alpha30.5.md", "docs/testing-alpha30.5.md"):
|
||||||
|
files[name] = (ROOT / name).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,2 @@
|
|||||||
|
matplotlib==3.11.2
|
||||||
|
numpy==2.5.3
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#!/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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 list(data["variants"]) != list(LABELS):
|
||||||
|
raise ValueError(f"Unsupported atlas schema/variants: {source}")
|
||||||
|
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, 360),
|
||||||
|
(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, 384)
|
||||||
|
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")
|
||||||
|
|
||||||
|
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