Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4d5e7c3c8 |
@@ -1,15 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{json,yml,yaml,toml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,9 +0,0 @@
|
||||
* text=auto
|
||||
*.java text eol=lf
|
||||
*.json text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.md text eol=lf
|
||||
gradlew text eol=lf
|
||||
gradlew.bat text eol=crlf
|
||||
*.png binary
|
||||
*.jar binary
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Signaler un comportement incorrect et le reproduire
|
||||
title: "[Bug] "
|
||||
---
|
||||
|
||||
## Résultat observé
|
||||
|
||||
Décrire le problème et son impact sur le jeu.
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
Décrire le comportement correct.
|
||||
|
||||
## Étapes de reproduction
|
||||
|
||||
1. Ouvrir…
|
||||
2. Faire…
|
||||
3. Observer…
|
||||
|
||||
## Environnement
|
||||
|
||||
- Version de Minecraft :
|
||||
- Version de Sanctuary ou commit :
|
||||
- Version de Fabric Loader et Fabric API :
|
||||
- Client seul ou serveur dédié :
|
||||
- Autres mods pertinents :
|
||||
- Fréquence : toujours / parfois / une fois :
|
||||
|
||||
## Monde concerné
|
||||
|
||||
Pour un problème de terrain, d'entités ou de position :
|
||||
|
||||
- Seed :
|
||||
- Dimension et coordonnées :
|
||||
- Monde nouvellement créé ou sauvegarde existante :
|
||||
- Type de monde et paramètres Sanctuary :
|
||||
|
||||
## Logs et captures utiles
|
||||
|
||||
Ajouter uniquement les éléments nécessaires à la reproduction. Retirer les secrets et informations personnelles. Indiquer si le problème survient aussi dans une sauvegarde de développement neuve, si ce test a été fait.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Fonctionnalité
|
||||
about: Proposer un incrément jouable de Sanctuary
|
||||
title: "[Feature] "
|
||||
---
|
||||
|
||||
## Ce que le joueur doit pouvoir faire
|
||||
|
||||
Décrire le résultat souhaité et son utilité en quelques phrases.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Indiquer les systèmes concernés et les dépendances éventuelles. Faire un ticket distinct pour les idées qui peuvent être livrées séparément.
|
||||
|
||||
## Critères d'acceptation
|
||||
|
||||
- [ ] Un comportement observable et vérifiable.
|
||||
- [ ] Un cas limite important, si nécessaire.
|
||||
|
||||
## Contexte et références
|
||||
|
||||
Exemples, croquis, liens vers la vision, ancien fichier ou commit utile. Pour la génération : seed, coordonnées, dimensions et configuration si elles sont connues.
|
||||
|
||||
## Décisions encore ouvertes
|
||||
|
||||
Valeurs d'équilibrage, règles, interface ou contraintes qui doivent être fixées pendant le ticket.
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Build Sanctuary
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Verify and build
|
||||
run: ./gradlew --no-daemon check build assemblePack
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
.DS_Store
|
||||
.gradle/
|
||||
**/build/
|
||||
**/run/
|
||||
**/out/
|
||||
.idea/
|
||||
*.iml
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.mrpack
|
||||
logs/
|
||||
crash-reports/
|
||||
saves/
|
||||
screenshots/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
__pycache__/
|
||||
@@ -1,19 +0,0 @@
|
||||
# Sanctuary Beta — travail par tickets
|
||||
|
||||
Ce dépôt contient le pack Sanctuary et ses mods. Lire `README.md`, puis le ticket concerné.
|
||||
La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités futures.
|
||||
|
||||
- Avant une modification, lire l'état Git et préserver les changements existants.
|
||||
- Un ticket correspond à un résultat jouable ou vérifiable et à une branche `codex/<sujet>`.
|
||||
- Garder les changements ciblés. Le dépôt voisin `26.2` est une référence historique, pas une cible de modification ou de déploiement.
|
||||
- Les sources Java sont dans `mods/<mod>/`, le manifeste du pack dans `packwiz/`.
|
||||
- Les règles de jeu et sauvegardes font autorité côté serveur. Les mods autonomes gardent leur propre responsabilité.
|
||||
- Ne pas modifier un monde existant, régénérer des chunks, changer un format de sauvegarde ou activer une expansion sans contrat de migration explicite.
|
||||
- Garder les identifiants `sanctuary:*` stables et documenter toute évolution de la génération avec graine et version.
|
||||
- Ajouter les libellés FR/EN des nouvelles interfaces. Vérifier les dépendances pour la version Minecraft exacte ; aucune compatibilité supposée à partir du nom d'un mod.
|
||||
- Lancer `./gradlew check build` pour livrer du code, et `./gradlew assemblePack` si la distribution change. Ajouter seulement les tests utiles au comportement touché.
|
||||
- Pour chaque nouvelle livraison du mod, incrémenter `mod_version` et `pack_version` dans `gradle.properties`, synchroniser `packwiz/pack.toml` et documenter le changement. Une simple modification documentaire n'incrémente pas les binaires.
|
||||
- Le pack Beta suit un canal packwiz stable et une seule instance Prism. Pour une mise à jour demandée, suivre `docs/packwiz.md` : publier un artefact vérifié et immuable, avancer le canal, puis synchroniser l'instance existante en conservant ses sauvegardes et réglages.
|
||||
- Ne pas versionner de secrets, mondes, JAR générés ou dépendances téléchargées. Le wrapper Gradle fait exception.
|
||||
- Ne pas déployer dans une installation de jeu ou un serveur personnel sans demande correspondante. Les serveurs de test restent dans les dossiers de développement ignorés.
|
||||
- Une livraison explique le résultat, les vérifications effectuées et les limites encore ouvertes. Ne pas marquer une intention comme implémentée.
|
||||
-556
@@ -1,556 +0,0 @@
|
||||
# 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.
|
||||
- Répartit de grandes zones de plateaux naturels et de montagnes déformées en 3D,
|
||||
avec une échelle relative commune à Petit, Moyen et Grand.
|
||||
- Étend les hauts reliefs vers Y360 sans changer la hauteur du monde ; conserve
|
||||
les profondeurs, l’arrivée, les cours d’eau courbes et les infrastructures.
|
||||
- Conserve un filtrage des fragments rocheux détachés jusque dans les sommets.
|
||||
- Adapte la recherche aérienne aux plateaux disponibles : rejet grossier des
|
||||
emprises trop creuses, contrôle de hauteur précoce et recherche autour de l’île.
|
||||
- Corrige le veto de biome qui empêchait un sanctuaire minéral pourtant admis
|
||||
de se poser dans les marais souterrains.
|
||||
- Nouveau monde nécessaire pour tester le nouveau terrain ; aucun monde régénéré.
|
||||
|
||||
## 0.1.0-alpha.30.2
|
||||
|
||||
- Corniches locales dont la phase et l’espacement suivent un bruit 3D, sur le relief global conservé.
|
||||
- Filtrage borné des petits composants rocheux détachés dans la zone retouchée, indépendant des frontières de chunks.
|
||||
- Rivières courbes, tunnels, sanctuaire minéral et lieux aériens conservés ; nouveau monde pour l’essai.
|
||||
- Minecraft 26.3-pre-2 et les dépendances restent inchangés. Voir `docs/testing-alpha30.2.md` pour les validations effectivement réalisées.
|
||||
|
||||
## 0.1.0-alpha.30.1 — 2026-09-12
|
||||
|
||||
Retrait des rivières sur grille de la 30 ; brèches de cascade sur les anciennes
|
||||
rivières courbes et leurs bassins. Retour du relief global de la 28, sauf une
|
||||
bande étroite de plateaux naturels pour les accès aux traversées.
|
||||
Voir [le correctif](docs/generation-alpha30.1.md). Nouveau monde nécessaire.
|
||||
|
||||
## 0.1.0-alpha.30 — 2026-09-12
|
||||
|
||||
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
|
||||
à niveaux locaux et brèches de berge de cinq blocs maximum laissant couler l’eau.
|
||||
Recherche des traversées mieux répartie, sanctuaire minéral avec seconde recherche
|
||||
sur les flancs, mines vanilla mieux admises. Les navires conservent leur modèle
|
||||
et leur placement proche du rivage. Nouveau monde de test nécessaire.
|
||||
Voir [la génération](docs/generation-alpha30.md) et [les essais](docs/testing-alpha30.md).
|
||||
|
||||
## 0.1.0-alpha.29 — 2026-09-12
|
||||
|
||||
Petites corniches variables sur le relief 3D de la28 ; correction des traversées
|
||||
et de l’admission des mines natives. Voir [le périmètre](docs/generation-alpha29.md)
|
||||
et [les essais](docs/testing-alpha29.md). Nouveau monde de test nécessaire.
|
||||
|
||||
## 0.1.0-alpha.28 — 2026-09-12
|
||||
|
||||
Spéléologie par bruits natifs, déformations volumiques 3D et failles obliques bornées ;
|
||||
bateaux rapprochés et îlots au-dessus de Sanctuary. Marais réduits. Mines,
|
||||
huttes et donjons vanilla adaptés aux souterrains ; filons natifs selon la
|
||||
profondeur dans la roche, dans l’île et les expansions. Nouveau monde de test.
|
||||
Voir [génération](docs/generation-alpha28.md) et [validation](docs/testing-alpha28.md).
|
||||
|
||||
## 0.1.0-alpha.27 — 2026-09-12
|
||||
|
||||
Retour à la base alpha.24 après rejet visuel de l’alpha.25. Le relief et les
|
||||
souterrains de la 24 sont conservés ; la montgolfière est retirée. Première
|
||||
passe limitée aux marais souterrains et à des épaves vanilla redressées
|
||||
et restaurées pour les navires marchands. Les nouvelles
|
||||
montagnes, cavités et garanties océaniques propres à la 25 ne sont pas reprises.
|
||||
Alpha.26 sautée ; essai sur nouveau monde, sans migration. Voir le
|
||||
[contrat alpha.27](docs/generation-alpha27.md) et les [vérifications](docs/testing-alpha27.md).
|
||||
|
||||
## 0.1.0-alpha.20 — 2026-09-10
|
||||
|
||||
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
|
||||
infrastructures, les espaces publics et les réseaux souterrains. Palettes
|
||||
propres aux usages : chêne, bouleau, granit, diorite, andésite et tuf ;
|
||||
fondations maçonnées en briques de pierre conservées.
|
||||
- Angles coupés, baies en saillie, portiques, retraits, loggias, toitures
|
||||
nervurées et verrières avec passage lumineux. Sols et plafonds détaillés,
|
||||
corniches, encadrements, murets et blocs ciselés ; bordures de routes en
|
||||
briques de tuf et accents ciselés.
|
||||
- Vitres, barreaux, clôtures, murs bas et angles d’escaliers raccordés depuis
|
||||
la géométrie complète de chaque pièce avant son découpage entre chunks.
|
||||
- Nouveau codec `sanctuary:island_v20`, journal et pièces distincts. Les mondes
|
||||
19 et antérieurs gardent leurs rendus et futurs chunks. Les plans de terrain,
|
||||
d’hydrologie et d’implantation restent ceux de la génération précédente.
|
||||
Personnaliser conserve les trois tailles et les structures expérimentales.
|
||||
|
||||
`check build assemblePack` passe, avec huit GameTests généraux. Création et
|
||||
réouverture de la ville et des infrastructures passent ; les rapports 19 restent
|
||||
identiques aux témoins publiés. Option désactivée et Petit/graine42 passent.
|
||||
JAR, MRpack et ZIP Prism vérifiés ; release et canal packwiz publiés. Deux
|
||||
synchronisations isolées puis deux dans la même instance Prism passent, avec
|
||||
925 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
|
||||
[vérifications et limites](docs/testing-alpha20.md). Les fonctions de la bêta
|
||||
restent à construire ; aucun nouveau système économique n’est activé.
|
||||
|
||||
## 0.1.0-alpha.19 — 2026-09-09
|
||||
|
||||
- Quatorze nouvelles infrastructures abandonnées sur l’île initiale, avec pièces,
|
||||
accès, mobilier, secrets et outils vanilla associés. La présence varie selon
|
||||
la graine et le relief ; les quatorze types ont été rencontrés en tests natifs.
|
||||
- Voies de fret aux rails incomplets et chemins entre destinations réelles.
|
||||
Le témoin Moyen, graine 0, contient treize lieux et sept liaisons, dont un
|
||||
fret de 499 blocs entre dépôt et pompage. Tracés enregistrés par chunk.
|
||||
- Option FR/EN « Structures expérimentales » dans le bouton natif Personnaliser,
|
||||
trois tailles conservées. Codec `sanctuary:island_v19` et journal 19 ; les
|
||||
anciens mondes gardent leurs plans et futurs chunks. Un nouveau monde avec
|
||||
l’option désactivée conserve le socle urbain sans les expériences 17/19.
|
||||
- Commande opérateur `/sanctuary structures` pour relever les entrées.
|
||||
La ville et ses sous-sols sont conservés ; les systèmes de la bêta restent
|
||||
à construire.
|
||||
|
||||
`check build assemblePack` passe, dont les huit GameTests généraux. Les trois
|
||||
formats sont vérifiés nativement ; le témoin moyen se rouvre avec ses 112 pièces,
|
||||
13 coffres, 68 cibles accessibles et la modification témoin. Les identités 17
|
||||
restent identiques à la release publiée. JAR, MRpack et ZIP Prism vérifiés ;
|
||||
release et canal packwiz publiés. Deux synchronisations isolées puis deux dans
|
||||
la même instance Prism passent, avec 767 fichiers personnels et réglages suivis
|
||||
conservés. Voir [les preuves et limites](docs/testing-alpha19.md).
|
||||
|
||||
## 0.1.0-alpha.17 — 2026-09-09
|
||||
|
||||
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
|
||||
salles étranges, infrastructures abandonnées. Accès depuis la surface,
|
||||
couloirs et embranchements, outils vanilla utilisables, mobilier et traces de
|
||||
chantier. Placement borné, hors ville et eaux protégées, avec appuis vérifiés.
|
||||
- Option FR/EN « Structures expérimentales » dans Personnaliser, enregistrée
|
||||
avec la taille du nouveau monde. Les connexions entre réseaux, le réacteur,
|
||||
la progression et les reliques ne sont pas encore implémentés.
|
||||
- Codec `sanctuary:island_v17`, pièces natives indépendantes, journal 17.
|
||||
Les mondes 16 et antérieurs conservent leurs plans et futurs chunks.
|
||||
L’option ne supprime ni n’ajoute rétroactivement des structures.
|
||||
|
||||
`check build assemblePack` passe. Création et réouverture natives : 15 pièces,
|
||||
7 639 blocs contrôlés, 49 cibles accessibles, 12 appuis et 3 coffres conservés.
|
||||
Le cas désactivé et la sauvegarde des trois tailles passent. La génération 16
|
||||
conserve les empreintes natives et le journal des témoins publiés. Artefacts
|
||||
vérifiés, release et canal packwiz publiés. Même instance Prism synchronisée
|
||||
deux fois, 767 fichiers personnels et réglages suivis conservés. Voir
|
||||
[le protocole et les coordonnées](docs/testing-alpha17.md).
|
||||
|
||||
## 0.1.0-alpha.16 — 2026-09-09
|
||||
|
||||
- Ville compacte obligatoire sur la nouvelle île initiale, structures activées.
|
||||
Recherche naturelle puis réservation architecturale locale si nécessaire ;
|
||||
les villes des continents restent conditionnelles.
|
||||
- Grande piscine couverte avec bassin de 50 × 25 × 4 blocs, promenades et
|
||||
marches. Parkings profonds de 3 à 8 plateformes, grande salle sèche et égouts
|
||||
ouverts vers l’extérieur avec écoulements natifs. Ces équipements restent
|
||||
conditionnés par le relief, leurs fondations et un budget de recherche borné.
|
||||
- Plans d’étages alternés, sanitaires clos et lits contre les murs, avec
|
||||
circulation, orientations et appuis du mobilier contrôlés.
|
||||
- Nouveau codec `sanctuary:island_v16`, journal16 et pièces natives propres.
|
||||
Les générations publiées conservent leurs calculs, rendus et futurs chunks.
|
||||
Un nouveau monde est nécessaire pour les nouveautés ; aucune conversion.
|
||||
|
||||
`check build assemblePack` passe, ainsi que création/réouverture : 71 pièces,
|
||||
5 coffres et 74 appuis conservés. Piscine retenue et deux écoulements extérieurs
|
||||
vérifiés après 120 ticks réels. Six reliefs contrôlés, avec piscines dans trois
|
||||
cas et villes dans les six. Compatibilité 15 vérifiée contre ses empreintes
|
||||
publiées. Release et canal packwiz publiés ; même instance Prism synchronisée
|
||||
deux fois, avec les 767 fichiers personnels et réglages suivis conservés. Voir
|
||||
[les preuves et limites alpha.16](docs/testing-alpha16.md).
|
||||
|
||||
## 0.1.0-alpha.15 — 2026-09-09
|
||||
|
||||
- Six familles de bâtiments reprennent leurs intérieurs : vraies pièces et
|
||||
portes, seuils, vestibules, couloirs, escaliers, sols, plafonds et mobilier
|
||||
adapté. Les parcours et les approches des lits sont vérifiés.
|
||||
- Réseaux souterrains d’égouts, passages, garages et pool rooms ; descentes
|
||||
de surface, bassins contenus et appuis courts ancrés lorsque la roche manque.
|
||||
- Petite ville compacte, rues et carrefours jointifs, terrasses dégagées et
|
||||
ponts intégrés au réseau. Correction des charnières, sièges et appuis des
|
||||
lanternes ; grande salle sèche dans les tunnels pour une future machine.
|
||||
- Codec `sanctuary:island_v15`, journal15 et pièces natives versionnées.
|
||||
Les anciennes générations gardent leurs plans, rendus et sauvegardes.
|
||||
Le contrat de reprise certifiée du vide reste strictement lié à sa génération.
|
||||
|
||||
`check build assemblePack`, les huit tests moteur et les scénarios de création
|
||||
et réouverture passent : 76 pièces natives et cinq coffres conservés, rues et
|
||||
intérieurs accessibles, bassins contenus. Release et canal packwiz publiés ;
|
||||
même instance Prism synchronisée deux fois, 767 fichiers personnels et réglages
|
||||
suivis conservés. Le JAR d’essai anticipé est sauvegardé séparément. Voir
|
||||
[les preuves et limites alpha.15](docs/testing-alpha15.md).
|
||||
|
||||
## 0.1.0-alpha.14 — 2026-09-09
|
||||
|
||||
- Biome Lost City et quartiers procéduraux contemporains : poches sur l’île
|
||||
initiale et profil `lost_city` pour les nouvelles expansions. Dix-sept types
|
||||
de parcelles, dont quatre parcs, ponts, parkings souterrains conditionnels,
|
||||
fouilles, piscines, casernes, halls, hôtels, bibliothèques et cuisines.
|
||||
- Palette dominée par la pierre, la pierre lisse et les briques de pierre,
|
||||
intérieurs accessibles, ruines déterministes et coffres à butin vanilla
|
||||
différé. Zombies vanilla par spawn naturel ; aucun spawner ni zombie spécial.
|
||||
- Nouveau codec public `sanctuary:island_v14` et journal propre de génération
|
||||
14. Les générations 12/13 restent chargeables sans conversion. Les nouveautés
|
||||
demandent un nouveau monde ; aucune sauvegarde personnelle n’est modifiée.
|
||||
- Expansions 64/128/256/512/1 024 ; `quick` sans taille tire selon les poids
|
||||
40/30/18/9/3, avec résultat stable pour la graine et l’identifiant réservés.
|
||||
Recherche proche dans le secteur demandé, séparation circulaire des emprises
|
||||
de terrain et de décoration, sans réserver tout le voisinage de lecture.
|
||||
- Contrat de reprise différée du vide : uniquement les chunks FULL prouvés
|
||||
vierges et vides d’un nouveau monde 14, certificat lié à un jeton immuable du
|
||||
journal, puis réouverture et nouvelle vérification. Une présence de terrain,
|
||||
d’entité, de POI, de ticks ou une preuve manquante arrête cette reprise.
|
||||
|
||||
`check build assemblePack`, les huit tests moteur, la création et la réouverture
|
||||
de Lost City passent. La reprise du vide conserve 154 chunks FULL éclairés
|
||||
correctement et un bloc témoin après réouvertures. JAR, MRpack et amorçage Prism
|
||||
publiés et vérifiés, canal packwiz avancé. Deux synchronisations isolées puis
|
||||
deux dans la même instance Prism Mac passent, avec 713 fichiers personnels et
|
||||
réglages suivis conservés. Aucun essai Windows natif effectué. Voir les
|
||||
[preuves et limites alpha.14](docs/testing-alpha14.md).
|
||||
|
||||
## 0.1.0-alpha.13.1 — 2026-09-09
|
||||
|
||||
- Corrige l’arrêt des expansions sous Windows : le journal ne tente plus
|
||||
d’ouvrir son dossier comme un fichier pour le synchroniser. Cette opération
|
||||
reste exigée sur les systèmes de fichiers utilisant le séparateur Unix.
|
||||
- Écriture du fichier synchronisée, remplacement atomique et relecture stricte
|
||||
conservés. Les véritables erreurs de stockage arrêtent toujours les expansions.
|
||||
- Après mise à jour et réouverture du même monde, une réservation valide déjà
|
||||
enregistrée avant l’erreur est reprise automatiquement. Aucun changement de
|
||||
schéma du journal, de génération, de paramètres ou de chunks existants.
|
||||
|
||||
Le smoke du journal passe sur macOS, avec politique Windows simulée, erreurs
|
||||
après remplacement et récupération au rechargement. Aucun essai Windows natif
|
||||
n’a encore été réalisé. Voir le [dépannage](docs/alpha13-worldgen.md#correctif-alpha131--expansions-sous-windows)
|
||||
et les [contrôles ciblés](docs/testing.md#correctif-alpha131--journal-dexpansion-sous-windows).
|
||||
|
||||
## 0.1.0-alpha.13 — 2026-09-09
|
||||
|
||||
- Un seul choix public **Sanctuary**, avec le bouton natif **Personnaliser** :
|
||||
Petit (512 blocs), Moyen (724, par défaut), Grand (1 024). Le diamètre est
|
||||
enregistré dans le générateur ; confirmer ou annuler agit sur le monde à créer.
|
||||
- Île initiale issue de la densité et de l’hydrologie complète de Population,
|
||||
avec sa forêt de surface et les poches de biomes souterrains natives du
|
||||
laboratoire. Les eaux et les ressources restent dépendantes du terrain.
|
||||
- Reprise des outils d’expansion alpha.12.1 dans ce même monde : parent,
|
||||
direction, climat, taille, relief, distance facultative et recherche de sites
|
||||
libres, sans remplacer les chunks existants. Le déblocage collectif reste futur.
|
||||
- Nouveau codec `sanctuary:island_v13`, racine de génération 13 et journal
|
||||
`data/sanctuary-world-v13/expansions.json`. Les extensions restent de génération
|
||||
12. Anciens codecs et presets conservés mais cachés de la création ; aucun
|
||||
monde existant converti, agrandi ou régénéré.
|
||||
- La suite GameTest unifiée devient le défaut de `check` et `runGameTest` ; les
|
||||
essais historiques demandent `-PsanctuaryUnifiedTests=false`.
|
||||
|
||||
`check build assemblePack`, interface client, trois tailles et rechargement des
|
||||
expansions vérifiés. Moyen : 63,66 s pour créer puis charger 289 chunks FULL
|
||||
autour du spawn sur la graine 0, 7,82 s pour les 80 suivants ; mesure serveur,
|
||||
sans garantie de FPS ni charge multijoueur. Release, MRpack et canal packwiz
|
||||
publiés ; même instance Prism synchronisée deux fois, avec 601 fichiers
|
||||
personnels et réglages suivis conservés.
|
||||
Voir le [guide alpha.13](docs/alpha13-worldgen.md) et les [preuves](docs/testing.md).
|
||||
|
||||
## 0.1.0-alpha.12.1 — 2026-09-09 (historique)
|
||||
|
||||
- Corrige le blocage de toutes les directions lorsque du vide a déjà été
|
||||
généré : la recherche continue plus loin sur le même rayon, sans remplacer
|
||||
les chunks existants.
|
||||
- Paramètre final `distance` facultatif pour `quick`, `create`, `preview` :
|
||||
distance minimale entre centres en blocs, de 0 à 100 000 ; 0 ou omis = auto.
|
||||
- Recherche limitée à 32 positions et un budget global de lecture de 30 secondes.
|
||||
Les erreurs disque et timeouts interrompent la demande ; seul un emplacement
|
||||
occupé permet de poursuivre. La graine et le profil sont conservés.
|
||||
- Compatibilité des commandes précédentes et des mondes laboratoire alpha.12 :
|
||||
journal v1 et génération 12 inchangés, sans migration ni régénération.
|
||||
|
||||
Les contrôles moteur de création et de rechargement passent sur la graine 42,
|
||||
avec huit premiers sites occupés et distance explicite. `check build assemblePack`
|
||||
passe, avec les douze tests historiques. Release, MRpack et canal publiés ;
|
||||
la même instance Prism est synchronisée deux fois et ses 466 fichiers personnels
|
||||
et réglages suivis sont conservés. Voir [WG-14](docs/backlog.md).
|
||||
|
||||
## 0.1.0-alpha.12 — 2026-09-09 (historique)
|
||||
|
||||
- Nouveau preset Sanctuary — Laboratoire, distinct de Petit/Grand. Outils
|
||||
opérateur `quick`, `create`, `preview`, `list`, `visit`, `resume` pour tester
|
||||
les expansions sans blocs de commande ni économie préalable.
|
||||
- Îles de 64, 128, 256 ou 512 blocs de diamètre nominal ; parent et direction
|
||||
enregistrés, climat libre parmi douze profils. La boussole climatique reste
|
||||
un raccourci facultatif ; les branches peuvent composer plusieurs couronnes.
|
||||
- Nouveaux reliefs hauts : naturel, pics, canyon, volcan. Le relief naturel est
|
||||
aussi testé sur l’île initiale du laboratoire. Palette volcanique et petites
|
||||
poches facultatives de fluides, sans ajout de fondations artificielles.
|
||||
- 34 biomes vanilla déclarés dès le chargement, avec surface et poches souterraines.
|
||||
Sélection de structures vanilla selon biomes, emprise et fondations : villages,
|
||||
temples de désert/jungle, cabanes de marais et camps abandonnés.
|
||||
- Journal serveur v1 atomique, paramètres immuables, reprise des réservations,
|
||||
refus des chunks existants ou en cours de chargement/déchargement, vide compris.
|
||||
Aucun changement des anciens générateurs ni migration d’une sauvegarde.
|
||||
|
||||
Validation locale : `check build assemblePack`, douze tests moteur Grand de
|
||||
graine 0, smokes de journal/relief, expansions et second processus de rechargement
|
||||
sur la graine 42 avec camp et butin natifs. Les essais restent sans client ni
|
||||
joueurs connectés. Release, MRpack et canal packwiz publiés ; la même instance
|
||||
Prism est synchronisée deux fois avec 466 fichiers personnels et réglages
|
||||
suivis conservés. Recherche collective,
|
||||
boîtes postales, Void, hydrologie étendue et autres structures restent à construire.
|
||||
Voir [le contrat du laboratoire](docs/alpha12-laboratory.md).
|
||||
|
||||
## 0.1.0-alpha.11 — 2026-09-09
|
||||
|
||||
- Deux choix à la création : **Petit** (512 blocs de diamètre nominal)
|
||||
et **Grand** (environ 724 blocs), Grand par défaut. Leur aire nominale varie
|
||||
d’un facteur deux, sur une hauteur de 384 blocs. Les repères internes 5/10
|
||||
ne désignent pas un nombre garanti de joueurs.
|
||||
- Nouveau réglage `sanctuary:population_10` pour Grand. Les anciens formats
|
||||
20/100 disparaissent des choix publics, mais restent chargeables avec leurs
|
||||
identifiants et leur relief enregistrés. Aucune île existante n’est réduite.
|
||||
- Cache des coins d’interpolation conservant l’arithmétique exacte du moteur,
|
||||
borné à 192 Kio par thread. Les plans régionaux déjà calculés restent lisibles
|
||||
pendant le calcul d’une nouvelle région ; le sampler est compilé une seule
|
||||
fois par plan.
|
||||
- À géométrie 20 identique, création et 289 chunks : 407,43 → 62,54 s
|
||||
(−84,65 %). Exploration de 80 chunks : 103,14 → 14,05 s. Le nouveau Grand
|
||||
atteint 62,67 s sur le protocole de départ. Une seule mesure par version,
|
||||
graine 0, M1/8 Gio : ce sont des temps serveur jusqu’à FULL, pas des FPS
|
||||
ni une garantie de délai en jeu.
|
||||
|
||||
Validation : `check build assemblePack`, douze tests moteur pour Petit et
|
||||
Grand, 1 800 ticks de fluides, persistance des quatre références et
|
||||
3 183 804 comparaisons bit à bit de densités réelles, plus 122 528 synthétiques.
|
||||
Les sources historiques de forme et les anciens réglages sont conservés.
|
||||
Voir [Validation](docs/testing.md) et [Distribution](docs/packwiz.md).
|
||||
Le canal stable est publié et la même instance Prism est synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## 0.1.0-alpha.10 — 2026-09-09
|
||||
|
||||
- Trois tailles de départ : **Sanctuary (20 joueurs)** par défaut, plus les
|
||||
variantes 5 et 100 joueurs. Diamètres nominaux de 512, 1 024 et environ
|
||||
2 290 blocs, hauteur de 384 blocs ; aucune limite de connexions modifiée.
|
||||
- Nouvelles générations `sanctuary:population_5`, `population_20` et
|
||||
`population_100`. Forêts, cavités, décorations et plans d’eau/lave couvrent
|
||||
la nouvelle emprise par régions locales ; une structure hydraulique qui
|
||||
dépasse son domaine est écartée en entier.
|
||||
- Capacité persistante sur disque, ancien réglage alpha.9 conservé ;
|
||||
212 anciens fichiers de production inchangés et huit fichiers de liaison
|
||||
ou d’interface adaptés. Aucune ancienne île agrandie ou régénérée.
|
||||
|
||||
Validation : `check build assemblePack` réussi, puis onze tests moteur
|
||||
réussis pour chacun des profils 5/20/100 sur la graine `0`, avec 1 800 ticks
|
||||
réels. Le MRpack et le ZIP d’amorçage sont vérifiés localement. Les mesures
|
||||
sont limitées aux chunks inspectés ; elles ne constituent ni un inventaire
|
||||
global ni une simulation de cent joueurs. Le premier plan local reste coûteux
|
||||
et la génération en masse reste à optimiser. Publication et synchronisations
|
||||
du canal sont consignées séparément dans [Distribution](docs/packwiz.md).
|
||||
|
||||
## 0.1.0-alpha.9 — 2026-09-09
|
||||
|
||||
- Cavités luxuriantes éclairées par leur végétation, secteurs à spéléothèmes
|
||||
et géodes d’améthyste recherchés sous l’île, sur des supports naturels.
|
||||
- Province de soufre limitée dans les profondeurs ; geysers utilisant
|
||||
les blocs et conditions de fonctionnement de Minecraft 26.3-pre-2.
|
||||
- Forêts et rivière de surface conservées ; recherche de lacs plus profonds
|
||||
et volumineux et de groupes facultatifs de bassins en terrasses reliés par
|
||||
des cascades. Les « rizières » désignent le relief aquatique, sans culture
|
||||
de riz ni mécanique d’ascenseur ajoutée au joueur.
|
||||
- Nouvelle clé `sanctuary:sanctuary_cavern`, réservée aux nouveaux mondes.
|
||||
Les générations jusqu’à l’alpha.8 restent séparées ; aucune sauvegarde
|
||||
existante n’est convertie ou régénérée.
|
||||
|
||||
Validation : au moins dix tests requis réussis sur chacune des graines `0`,
|
||||
`42` et `8675309`, avec 1 800 ticks de fluides ; `check build assemblePack`
|
||||
réussi. Les preuves cumulées comprennent deux bassins en terrasses reliés,
|
||||
un geyser dormant puis éruptif, des baies lumineuses, des spéléothèmes et de
|
||||
l’améthyste bourgeonnante. Les mesures et leurs périmètres figurent dans
|
||||
[Validation](docs/testing.md). Release et canal packwiz publiés ; double
|
||||
synchronisation isolée puis dans la même instance Prism réussie, avec 260
|
||||
fichiers personnels et réglages conservés. Aucun site ni stock n’est garanti
|
||||
sur chaque graine.
|
||||
|
||||
## 0.1.0-alpha.8 — 2026-09-09
|
||||
|
||||
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
|
||||
de la roche pour ouvrir l’intérieur de l’île.
|
||||
- Nappes de tuf, cobblestone, pierre moussue et boue compactée dans les étages
|
||||
humides ; ardoise des abîmes, roche noire et basalte plus bas. Les poches de
|
||||
soufre et de cinabre peuvent apparaître à des altitudes plus accessibles.
|
||||
- Davantage d’essais de petits filons de charbon, fer et cuivre, y compris
|
||||
au contact de l’air. Ajout de l’émeraude et adaptation des huit familles de
|
||||
minerais aux nouvelles roches, sans quota de remplissage.
|
||||
- Décoration des corniches revue avec de gros champignons, des petits
|
||||
champignons plus dispersés et des sols de mousse, podzol et mycélium.
|
||||
- Bassins plus grands et recherche d’une
|
||||
longue rivière calme au tracé arrondi, de largeur variable, soutenue par la
|
||||
roche, avec bassins terminaux, berges progressives et source en paroi.
|
||||
- Recherche prioritaire de la poche de lave couverte entre Y=64 et Y=160,
|
||||
avec recherche plus haute si aucun site profond ne convient. Les sources
|
||||
profondes supplémentaires produisent leurs coulées par les ticks vanilla.
|
||||
- Nouvelle clé `sanctuary:sanctuary_rift` ; les anciennes générations jusqu’à
|
||||
l’alpha.7 restent séparées. Cet incrément nécessite un **nouveau monde Sanctuary**.
|
||||
|
||||
Validation : tests serveur réussis sur chacune des graines `0`, `42` et
|
||||
`8675309`, avec les bassins terminaux, la continuité des champignons et
|
||||
1 800 ticks réels de fluides. `check build assemblePack` réussi sur la graine
|
||||
`42`. Les mesures et leurs limites figurent dans [Validation](docs/testing.md).
|
||||
Publication immuable et canal packwiz vérifiés, avec deux synchronisations
|
||||
dans un dossier neuf puis deux dans la même instance Prism ; sauvegardes et
|
||||
réglages conservés.
|
||||
Aucune rivière, coulée ou réserve de ressources n’est garantie pour chaque graine.
|
||||
|
||||
## 0.1.0-alpha.7 — 2026-09-08
|
||||
|
||||
- Forêts de chênes plus denses, y compris au cœur et sur les hauteurs de l’île,
|
||||
avec bouleaux, clairières fleuries et bois plus secs.
|
||||
- Vrais chênes noirs, champignons, podzol et mycélium sur les corniches intérieures
|
||||
humides ; mousse plus ponctuelle et rares poches de jungle/bambou.
|
||||
- Un emplacement facultatif pour un arbre remarquable (cerisier favorisé,
|
||||
épicéa, jungle, acacia, palétuvier ou chêne pâle), sans terrain ajouté.
|
||||
- Nappes de pierre, andésite, diorite et granite sur les affleurements ; dépôts
|
||||
épais de sable, gravier et argile associés aux bassins. Rares matières de
|
||||
soufre dans les profondeurs, sans grands bassins souterrains.
|
||||
- Petits filons de charbon, fer et cuivre autorisés à affleurer, avec des essais
|
||||
sur les roches de surface et les corniches pour les rendre repérables.
|
||||
- Nouvelle clé `sanctuary:sanctuary_woodland`, avec formes, hauteur, ciel et
|
||||
plans d’eau conservés. Créer un **nouveau monde Sanctuary** ; les sauvegardes
|
||||
alpha.6 et antérieures conservent leur génération.
|
||||
|
||||
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
|
||||
`8675309`, arbres et minerais réellement observés, 1 800 ticks de fluides,
|
||||
et `check build assemblePack` réussi.
|
||||
|
||||
## 0.1.0-alpha.6 — 2026-09-08
|
||||
|
||||
- Palette tempérée plus douce, avec des couleurs communes pour l’herbe, le
|
||||
feuillage et l’eau. Dappled Forest quitte l’île initiale et reste réservé
|
||||
aux futurs continents ; les sauvegardes alpha.5 le conservent.
|
||||
- Prairies fleuries et affleurements rocheux plus étendus, avec une répartition
|
||||
des biomes selon l’altitude. Les corniches inférieures peuvent porter de la
|
||||
mousse, des fougères, des fleurs et des petits arbustes.
|
||||
- Sources recherchées aussi dans les parois des strates inférieures. Les
|
||||
cascades peuvent rejoindre un palier ou descendre dans le vide.
|
||||
- Retrait des courts ruisseaux à niveau constant : l’île conserve ses étangs
|
||||
et petits lacs. La grande rivière avec source, chute et bassin reste à
|
||||
développer ; elle n’est pas activée dans cette livraison.
|
||||
- Nouvelle clé `sanctuary:sanctuary_layered`, avec les anciennes générations
|
||||
conservées. Créer un **nouveau monde Sanctuary** pour cet incrément.
|
||||
|
||||
Validation : dix tests serveur réussis sur chacune des graines `0`, `42` et
|
||||
`8675309`, avec 1 800 ticks de fluides, contrôles des corniches et respect des
|
||||
modifications ultérieures aux sources. `check build assemblePack` réussi.
|
||||
|
||||
## 0.1.0-alpha.5 — 2026-09-08
|
||||
|
||||
- Plages plus larges et irrégulières, en nappes de sable, gravier et roche, avec
|
||||
des transitions herbeuses. Dépôts de 3 à 5 blocs au lieu d’une seule couche.
|
||||
- Étangs et lacs moins creusés, rives variées et petites terrasses ; la canne
|
||||
à sucre reste soumise à ses conditions vanilla.
|
||||
- Quelques sources sortent de niches rocheuses naturelles à différentes
|
||||
altitudes. Leurs cascades peuvent descendre dans le vide et servir à circuler
|
||||
verticalement ; seuls les blocs sources sont posés par la génération.
|
||||
- Petite réserve de lave dans une niche rocheuse accessible lorsque le relief
|
||||
fournit un fond, des parois et une voûte adaptés.
|
||||
- Mosaïque de cinq biomes tempérés, dont Dappled Forest de Minecraft 26.3.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
|
||||
altitudes adaptées à l’île. Aucun quota ajouté après comptage des ressources.
|
||||
- Nouvelle clé de génération `sanctuary:sanctuary_natural` ; anciennes versions
|
||||
de terrain et d’hydrologie conservées pour les sauvegardes existantes.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour tester cette génération. La hauteur
|
||||
et le ciel validés en alpha.3/4 sont conservés. L’accès à l’End, les expansions
|
||||
et les grandes rivières en pente restent à développer.
|
||||
|
||||
Validation : dix tests serveur réussis sur chacune des graines 0, 42 et 8675309,
|
||||
avec 1 800 ticks réels de fluides. Les bassins et les réserves de lave restent
|
||||
stables ; les neuf cascades atteignent Y=0. `check build assemblePack` et les
|
||||
contrôles des couches de sédiments, des anciens générateurs et du pack réussissent.
|
||||
|
||||
## 0.1.0-alpha.4 — 2026-09-08
|
||||
|
||||
- Première hydrologie de surface : étangs, petits lacs et cours d’eau calmes
|
||||
adaptés au relief, avec vérification du fond et des berges.
|
||||
- Fonds de sable, gravier et argile, berges sableuses et canne à sucre
|
||||
placée après la végétation lorsque ses conditions vanilla sont réunies.
|
||||
- Hauteur, ciel et densité du terrain alpha.3 conservés ; nouveaux paramètres
|
||||
dédiés pour préserver la génération des anciennes sauvegardes.
|
||||
- Plan déterministe indépendant de l’ordre de génération des chunks, sans
|
||||
digue ajoutée, remplissage de cavité souterraine ni mer globale.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour tester l’hydrologie. Le pack continue
|
||||
à se mettre à jour dans la même instance Prism. Les grandes rivières en pente,
|
||||
cascades, océans et eaux des futurs continents restent à développer.
|
||||
|
||||
Validation : 10/10 tests serveur réussis sur chacune des graines 0, 42 et
|
||||
8675309, avec eau contenue après simulation réelle, continuité du cours d’eau
|
||||
et canne à sucre valide. Tests de forme et de déterminisme également réussis.
|
||||
Les cartes de blocs sont inspectées ; l’aspect dans le client reste à tester.
|
||||
|
||||
## 0.1.0-alpha.3 — 2026-09-08
|
||||
|
||||
- Nouveaux mondes Sanctuary sur 384 blocs de hauteur, avec l'île relevée de
|
||||
64 blocs et un dessous sculpté pour remplacer l'ancien fondu horizontal.
|
||||
- Nuages relevés et suppression du disque sombre de l'horizon et du
|
||||
noircissement d'altitude propres au vide dans ces nouveaux mondes.
|
||||
- Anciens paramètres de monde conservés : aucune translation de blocs ou
|
||||
modification de la hauteur des sauvegardes existantes.
|
||||
- Premier champ de recherche climatique documenté : nord froid, sud chaud,
|
||||
ouest sec, est humide et leurs combinaisons dans les quatre diagonales.
|
||||
- Relevé de développement des blocs et ressources après génération, avec
|
||||
emprise et complétude explicites. Aucun quota n'influence le terrain.
|
||||
|
||||
Les étangs, lacs de surface, rivière, berges à canne à sucre et plages de l'île
|
||||
tempérée restent à implémenter. Le contrat de cette hydrologie et de l'expansion
|
||||
est dans [Expansion et ressources](docs/expansion.md).
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour la hauteur et le ciel de l'alpha.3.
|
||||
Le pack se met à jour dans la même instance Prism.
|
||||
|
||||
Validation : build et pack réussis, 8 tests serveur réussis sur chacune des
|
||||
graines 0, 42 et 8675309, injections de rendu vérifiées dans le vrai client.
|
||||
Le relevé complet de la graine 0 et les limites des essais sont documentés dans
|
||||
[Validation](docs/testing.md) et [Expansion et ressources](docs/expansion.md).
|
||||
|
||||
## 0.1.0-alpha.2 — 2026-09-08
|
||||
|
||||
- Suppression du noyau elliptique qui ajoutait une masse artificielle au centre.
|
||||
- Sculpture progressive du contour par érosion en trois dimensions ; retrait
|
||||
des faces hautes et basses pour réduire les grandes tranches verticales.
|
||||
- Spawn recherché sur le terrain naturel, avec une surface de 3 × 3 privilégiée.
|
||||
- Canal packwiz stable et release versionnée, pour mettre à jour la même
|
||||
instance Prism avant chaque lancement.
|
||||
|
||||
Validation : `./gradlew check build assemblePack`, tests de densité compilée,
|
||||
coupes du générateur et tests serveur sur plusieurs graines. Voir
|
||||
[Validation](docs/testing.md) et [Distribution Prism](docs/packwiz.md).
|
||||
|
||||
Tester la génération dans un **nouveau monde Sanctuary**. Les chunks déjà
|
||||
générés conservent leur ancien terrain ; les sauvegardes ne sont pas régénérées.
|
||||
Minecraft reste en 26.3-pre-2, avec Fabric Loader 0.19.5.
|
||||
|
||||
## 0.1.0-alpha.1 — 2026-09-08
|
||||
|
||||
Première base indépendante de Sanctuary Beta pour Minecraft 26.3-pre-2 / Fabric.
|
||||
|
||||
- Construction Java 25 / Gradle reproductible, CI Gitea et modèles de tickets.
|
||||
- Vision du projet conservée et audit du générateur 26.2 documenté.
|
||||
- Preset Sanctuary : île flottante finie issue du bruit historique, forêt de
|
||||
départ utilisant les ressources vanilla et extérieur vide.
|
||||
- Spawn initial près de l'origine, recherché sur une surface pleine et libre.
|
||||
- Tests de forme et tests sur un vrai serveur Minecraft de développement.
|
||||
- Manifeste packwiz avec Fabric API épinglé et assemblage du mod local.
|
||||
|
||||
Validation : `./gradlew check build assemblePack` réussit. Le serveur de test
|
||||
valide le spawn `(0, 118, 0)`, 12 chunks extérieurs entièrement vides et le
|
||||
déterminisme du bruit compilé. Voir [Validation](docs/testing.md) pour les seeds,
|
||||
les limites des tests et les vérifications manuelles restantes.
|
||||
|
||||
Cette version prépare le terrain. L'hydrologie, les continents déverrouillables,
|
||||
TerraMix, la progression, l'économie et les autres dimensions restent à venir.
|
||||
Les mondes joués de 26.2 ne sont pas migrés par ce prototype.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Contribuer à Sanctuary
|
||||
|
||||
Sanctuary avance par petits tickets de fonctionnalités et de bugs sur le [Git du projet](https://git.botsu.net/koka/sanctuary-beta). Le [document de vision](docs/vision.md) explique la destination ; le [backlog de démarrage](docs/backlog.md) organise les premières étapes. Une idée décrite dans la vision n'est pas automatiquement demandée dans le ticket en cours.
|
||||
|
||||
## Ouvrir un ticket
|
||||
|
||||
Utiliser le modèle **Fonctionnalité** pour décrire ce que le joueur doit pouvoir faire et le modèle **Bug** pour un comportement incorrect. Privilégier un seul résultat observable par ticket.
|
||||
|
||||
Un ticket utile contient :
|
||||
|
||||
- le contexte et le comportement attendu, exprimés du point de vue du joueur ou de l'administrateur ;
|
||||
- le périmètre du changement et les éventuelles dépendances ;
|
||||
- quelques critères d'acceptation concrets ;
|
||||
- pour un bug, la version exacte, les étapes de reproduction, le résultat observé et les logs pertinents ;
|
||||
- pour la génération, la seed, les coordonnées, le type de monde et la configuration concernée.
|
||||
|
||||
Les nombres, ressources et interfaces encore incertains peuvent rester des hypothèses. Il faut les rendre explicites puis choisir la plus petite solution testable dans le périmètre du ticket. Les tickets distants peuvent être consultés et préparés pendant le développement ; leur publication et les messages adressés à d'autres personnes suivent la demande de l'auteur du travail.
|
||||
|
||||
## Réaliser un changement
|
||||
|
||||
1. Lire le ticket et les instructions du dépôt, puis examiner le code réellement concerné. Lorsqu'une ancienne version sert de référence, noter son chemin ou son commit et ne pas traiter ses anciennes procédures comme des consignes de déploiement du nouveau dépôt.
|
||||
2. Utiliser une branche descriptive, par exemple `codex/wg-main-island` ou `codex/fix-spawn-void`. Éviter les changements sans rapport avec le ticket.
|
||||
3. Construire un incrément jouable. Garder les systèmes futurs hors du chemin critique tant qu'ils ne sont pas nécessaires au comportement demandé.
|
||||
4. Exécuter les commandes de construction et les vérifications adaptées indiquées dans le README. Pour un comportement de jeu, compléter par une reproduction manuelle quand elle est nécessaire.
|
||||
5. Mettre à jour la documentation si le changement affecte l'installation, la configuration, les commandes ou le format d'une sauvegarde.
|
||||
6. Présenter le résultat avec ce qui a changé, pourquoi, les vérifications exécutées et les limites connues. Associer le ticket à la proposition de changement lorsqu'il existe.
|
||||
|
||||
Les commandes exactes de développement vivent dans le [README](README.md), afin de ne pas maintenir deux listes divergentes.
|
||||
|
||||
## Vérifier la génération du monde
|
||||
|
||||
Utiliser une sauvegarde de développement dédiée et conserver la seed des observations. Les vérifications pertinentes comprennent le spawn, les limites de l'île, le vide, les jointures de chunks, le comportement de l'eau, le redémarrage et l'arrivée de plusieurs joueurs.
|
||||
|
||||
Avant de modifier une stratégie de génération, préciser son effet sur les chunks existants. Les nouvelles expansions doivent préserver les constructions. Documenter les changements de format persistant et leur traitement ; ne pas promettre la compatibilité des anciennes sauvegardes sans l'avoir vérifiée.
|
||||
|
||||
Des tests automatisés sont utiles pour les invariants importants, par exemple le déterminisme des coordonnées, les limites géographiques ou la persistance d'un état. Ne pas multiplier les tests qui recopient simplement l'implémentation ou les vérifications sans rapport avec le changement.
|
||||
|
||||
## Ressources et intégrations
|
||||
|
||||
Avant de reprendre du code, des textures, de la musique, des modèles ou des configurations de l'historique et de la communauté, conserver leur provenance et respecter leur licence. Une ressource installée localement n'est pas automatiquement redistribuable dans le modpack.
|
||||
|
||||
Sanctuary, son modpack, It's Alive !, Only Fun et Master Key ont des responsabilités distinctes. Une intégration commence par un ticket qui précise la version supportée, la dépendance réelle et le comportement en son absence. L'ajout d'une dépendance ou d'un module doit répondre à un besoin livré.
|
||||
|
||||
## Signaler les résultats
|
||||
|
||||
Une description de changement doit être compréhensible sans lire la conversation de développement. Pour un bug, donner si possible un exemple avant/après. Indiquer ce qui a été testé réellement ; une compilation réussie ne prouve pas à elle seule qu'une génération est agréable ni qu'une session multijoueur fonctionne.
|
||||
|
||||
Les captures, logs et sauvegardes partagées doivent être limités au contexte utile et ne pas inclure de jetons, données d'authentification ou informations personnelles inutiles. Ne pas ajouter les répertoires d'exécution, les caches ou les mondes complets au dépôt par défaut.
|
||||
@@ -1,676 +0,0 @@
|
||||
Sanctuary 26.2 is licensed under GPL-3.0-or-later.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,731 +1,75 @@
|
||||
# Sanctuary
|
||||
# Sanctuary — historique des notes de livraison
|
||||
|
||||
Sanctuary est une extension gratuite de Minecraft qui transforme le jeu en un
|
||||
monde flottant d'exploration, de production et de progression collective.
|
||||
Les joueurs commencent ensemble sur Sanctuary Island et construisent les
|
||||
infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
|
||||
Publication rétrospective des livraisons locales après **alpha.30.7**, jusqu’à **beta.064**.
|
||||
|
||||
> Sanctuary is a free expansion of Minecraft that reshapes the game around a
|
||||
> floating world. Players begin together on Sanctuary Island, isolated in the
|
||||
> void. By exploring, building infrastructure and producing resources, they
|
||||
> progressively unlock new floating continents, each with its own geography.
|
||||
> Rather than a linear campaign, the server itself becomes a world that grows
|
||||
> through the action of its players.
|
||||
Cette branche contient uniquement des documents. Les tags `notes/beta.xxx` sont des repères documentaires ; leurs archives Gitea ne sont pas les sources historiques du mod. Les posts beta.060 et beta.061 utilisent les tags de code exacts déjà publiés. Les tags alpha et les artefacts existants restent immuables.
|
||||
|
||||
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
|
||||
conserve les intentions ; le [backlog](docs/backlog.md) suit les
|
||||
tickets. Les systèmes d’économie, de progression collective et les dimensions
|
||||
décrits dans la vision ne sont pas encore implémentés. **L’alpha.27 repart de
|
||||
l’alpha.24**, après le rejet du rendu de l’alpha.25. Cette première passe conserve
|
||||
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).
|
||||
**63 livraisons** sont recensées. Le chantier beta.021 a été finalisé en beta.023, sans livraison indépendante. L’alpha.30.7 possède déjà son [post de release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.7).
|
||||
|
||||
## Alpha.30.4 — massifs et ciel constructible
|
||||
Chaque texte décrit sa version historique ; les livraisons suivantes ont pu modifier les mêmes règles. Le détail des vérifications et limites reste accessible depuis chaque post. La préparation de ces notes ne rejoue pas les tests de jeu et ne publie pas les archives locales.
|
||||
|
||||
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.
|
||||
De larges provinces de relief font cohabiter les plateaux naturels issus de la
|
||||
24 et les montagnes déformées en 3D, jusqu’à proximité de Y360. Leur répartition
|
||||
est proportionnée aux trois tailles. Les petits fragments détachés restent filtrés.
|
||||
|
||||
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.3.md)
|
||||
et [les vérifications](docs/testing-alpha30.3.md).
|
||||
|
||||
## Alpha.30.1 — rivières courbes et relief global
|
||||
|
||||
Les rivières angulaires de la 30 sont retirées. Les anciens cours d’eau et
|
||||
bassins peuvent déborder par une courte brèche dans leur berge. Le relief 3D
|
||||
redevient global, avec une seule bande étroite de plateaux naturels pour
|
||||
faciliter les accès aux traversées.
|
||||
|
||||
Créer un **nouveau monde**. Voir [le correctif](docs/generation-alpha30.1.md)
|
||||
et [ses vérifications](docs/testing-alpha30.1.md).
|
||||
|
||||
## Alpha.30 — plateaux, rivières ouvertes et traversées
|
||||
|
||||
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
|
||||
reprennent leur place entre elles. Une rivière cherche un parcours à l’échelle
|
||||
de l’île. Des brèches courtes dans ses berges et celles des bassins laissent
|
||||
l’eau tomber sur les flancs ou dans le vide. Les traversées souterraines et
|
||||
le placement du sanctuaire minéral sont renforcés ; les navires restaurés
|
||||
restent proches des rivages.
|
||||
|
||||
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.md),
|
||||
[les essais et leurs limites](docs/testing-alpha30.md) et [packwiz](docs/packwiz.md).
|
||||
L’alpha.29 reste un point de reprise local non publié, intégré à cette livraison.
|
||||
Le [MRpack alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30/Sanctuary-0.1.0-alpha.30.mrpack)
|
||||
est publié ; le canal packwiz et l’instance Prism existante sur Mac sont
|
||||
synchronisés. Les essais natifs et le rechargement sont validés sur Mac.
|
||||
|
||||
|
||||
## Alpha.28 — grottes et lieux proches
|
||||
|
||||
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
|
||||
la roche, failles moins traversantes, reliefs déplacés et affaissés par du bruit 3D,
|
||||
bateaux près des berges et deux îlots au-dessus de Sanctuary. Les marais
|
||||
occupent moins de place. Mines, huttes et donjons vanilla bénéficient de
|
||||
contrôles adaptés aux cavités ; les filons suivent la profondeur de la masse
|
||||
rocheuse, y compris dans les expansions.
|
||||
|
||||
Créer un **nouveau monde**. Voir le [périmètre](docs/generation-alpha28.md),
|
||||
les [résultats effectivement vérifiés](docs/testing-alpha28.md) et la
|
||||
[distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.27 — retour au terrain de la 24
|
||||
|
||||
La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées
|
||||
et couches de planification de l’alpha.25. Les changements de relief seront
|
||||
repris séparément après un essai visuel. L’alpha.26 est sautée à la demande
|
||||
du créateur.
|
||||
|
||||
Les deux navires et les deux îlots restent présents, avec un seul îlot à
|
||||
cascade. Les marais occupent des volumes de biomes dans les cavités existantes,
|
||||
sans produire une nouvelle géométrie de grotte. Ce retour ne prétend pas
|
||||
remplacer le creusement Sanctuary par l’ensemble du moteur de grottes vanilla.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** ; tailles 512 / 724 / 1 024 et Moyen par
|
||||
défaut conservés. Le codec de terrain reste `sanctuary:island_v24` dans ce
|
||||
prototype alpha.27 ; la reprise des anciennes sauvegardes n’est pas un objectif.
|
||||
La distribution et les vérifications effectuées sont détaillées dans
|
||||
[les essais alpha.27](docs/testing-alpha27.md) et [packwiz](docs/packwiz.md).
|
||||
|
||||
## Alpha.24 — les lieux suspendus
|
||||
|
||||
La nouvelle génération `sanctuary:island_v24` ajoute une montgolfière, deux
|
||||
navires marchands habités et deux îlots riches en minerais autour de Sanctuary.
|
||||
Les îlots reçoivent de l’herbe, des fleurs et des petits arbres ; exactement
|
||||
l’un des deux possède une cascade. Chaque lieu contient un coffre, enterré
|
||||
dans les îlots. Il faut construire pour les atteindre et ramener les villageois.
|
||||
|
||||
Les cinq emplacements sont réservés dès la création du monde, même avant leur
|
||||
exploration. Les expansions respectent ces réservations ; les anciennes
|
||||
sauvegardes gardent leur génération. Créer un **nouveau monde Sanctuary** pour
|
||||
les découvrir, avec les structures et l’option expérimentale activées.
|
||||
Les tailles 512 / 724 / 1 024 et le bouton natif **Personnaliser** restent
|
||||
inchangés. Voir [le contrat](docs/generation-alpha24.md) et
|
||||
[les vérifications](docs/testing-alpha24.md). Les quatorze scénarios de génération,
|
||||
les 24 GameTests généraux, `check build assemblePack` et le MRpack passent sur
|
||||
Mac. Le [MRpack alpha.24](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.24/Sanctuary-0.1.0-alpha.24.mrpack)
|
||||
est publié, le canal packwiz et la même instance Prism sont synchronisés. Les
|
||||
925 fichiers personnels et réglages suivis sont conservés. Les essais sont
|
||||
réalisés sur Mac ; aucun essai Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.23.1 — livraison locale vérifiée
|
||||
|
||||
Le [chantier de génération](docs/generation-alpha23.1.md) réunit le Blocodex, une
|
||||
île initiale naturelle avec quelques secrets, le sanctuaire minéral, les océans
|
||||
et le catalogue des structures vanilla. Chaque nouvelle expansion acceptée, avec
|
||||
les structures activées, prévoit au moins une structure adaptée avant sa réservation.
|
||||
Les familles absentes des régions entièrement vérifiées sont favorisées.
|
||||
|
||||
`check build`, `assemblePack`, les essais natifs et le MRpack passent sur Mac.
|
||||
Voir les [vérifications et limites](docs/testing-alpha23.1.md). Créer un nouveau
|
||||
monde Sanctuary pour `island_v23` ; les anciennes sauvegardes gardent leur génération.
|
||||
La publication séparée de l’alpha.23.1 était bloquée par les identifiants Git.
|
||||
Ce socle est désormais inclus dans l’alpha.24 publiée ci-dessus.
|
||||
|
||||
## Alpha.23 — Blocodex et socle commun de données, livraison locale
|
||||
|
||||
Le Blocodex fait partie de Sanctuary et s’ouvre avec **B**, touche modifiable
|
||||
dans les contrôles. Son catalogue permet de rechercher et filtrer les blocs
|
||||
inconnus, vus, minés, déjà possédés, jetés ou posés. Le stock actuellement porté
|
||||
est affiché séparément de la possession passée. Aucun objet ni permission
|
||||
opérateur n’est nécessaire.
|
||||
|
||||
La mémoire est personnelle et conservée côté serveur. Les compteurs de minage
|
||||
et de jet viennent des statistiques Minecraft ; les observations et poses
|
||||
commencent avec cette version. Les sauvegardes précédentes gardent leur terrain
|
||||
et leur génération.
|
||||
|
||||
Le socle commun ajoute la distribution du terrain chargé par dimension et
|
||||
chunk, ainsi que les quantités d’items dans les inventaires connectés, les
|
||||
conteneurs chargés et au sol. La couverture et l’actualité accompagnent chaque
|
||||
relevé ; les coffres dont le butin n’est pas encore généré restent intacts.
|
||||
Les opérateurs disposent de `/sanctuary census`. Palettes de structures,
|
||||
expansions et shop pourront réutiliser ces données côté serveur.
|
||||
|
||||
Des relevés par jour UTC des activités de minage, pose, ramassage, jet et
|
||||
fabrication conservent des périodes de
|
||||
l’histoire du serveur. Le [contrat Blocodex](docs/blocodex.md) précise les APIs,
|
||||
la persistance et les exclusions. Shop, ballast, Backrooms et leurs règles de
|
||||
progression restent à construire ; la direction narrative est conservée dans
|
||||
la [cosmologie](docs/cosmologie.md). Le Blocodex initial est désormais inclus dans l’alpha.24 publiée sur le
|
||||
canal packwiz. `check build assemblePack`, les 24 GameTests natifs et
|
||||
le client graphique connecté passent ; voir [les essais alpha.23](docs/testing-alpha23.md).
|
||||
|
||||
La suite est en [conception](docs/structures-conception.md) :
|
||||
départ naturel, quelques lieux isolés, fonctions reproductibles et donjon majeur
|
||||
à définir. Ce cahier accompagne la poursuite des systèmes après la génération ;
|
||||
l’alpha.24 est la livraison actuelle.
|
||||
|
||||
## Alpha.22 — trouver le bon terrain
|
||||
|
||||
La nouvelle génération préfère refuser une ville ou un bâtiment facultatif
|
||||
qui ne trouve pas un emplacement adapté. Plus de ville forcée sur une réserve
|
||||
artificielle ; moins de terrassement, des fondations proches de la roche et des
|
||||
chemins qui suivent le relief. La grande traversée et la salle d’expansion sont
|
||||
réservées en priorité et peuvent exister sans ville au-dessus. Les autres lieux
|
||||
n’ont pas de quota garanti.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour utiliser ces règles. Les tailles
|
||||
512/724/1024 et le bouton natif **Personnaliser** restent inchangés ; les anciens
|
||||
mondes conservent leurs générateurs. La vision retient désormais **Minecraft
|
||||
comme archéologie d’un autre Minecraft** : chaque ruine témoigne d’une ancienne
|
||||
solution, et de ceux qui l’ont construite puis ont disparu.
|
||||
|
||||
Les trois tailles natives, la réouverture, le cas expérimental OFF et la
|
||||
compatibilité21 passent, ainsi que `check build assemblePack` et ses huit
|
||||
GameTests généraux. Le [MRpack alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
|
||||
est disponible, le canal packwiz et la même instance Prism sont synchronisés.
|
||||
Les925 fichiers personnels et réglages suivis sont conservés. Voir
|
||||
[la distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.21 — matériaux, quartiers et traversées
|
||||
|
||||
La ville revient à la pierre lisse, pierre, briques de pierre, andésite et chêne.
|
||||
Le tuf accompagne surtout les chemins et les sous-sols. Les toits et ornements
|
||||
sont raccordés à leurs appuis ; les pièces gardent leurs aménagements. La ville
|
||||
recherche des petits quartiers séparés par du terrain naturel, avec des chemins
|
||||
vers le cœur. Les lieux patrimoniaux recherchent davantage de raccords.
|
||||
|
||||
L’option expérimentale ajoute une recherche de galeries industrielles : chaussée
|
||||
cinq blocs, deux trottoirs, salles et escaliers de surface. Les placements sur
|
||||
pente sont assouplis et les destinations isolées sont prioritaires pour les chemins.
|
||||
Créer un **nouveau monde Sanctuary** pour ce générateur21. Les anciens mondes
|
||||
conservent leur génération. Les vérifications et coordonnées du
|
||||
témoin sont consignées dans [les essais](docs/testing-alpha21.md).
|
||||
|
||||
Le [MRpack alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
|
||||
est disponible. Le canal packwiz et la même instance Prism sont synchronisés ;
|
||||
les 925 fichiers personnels et réglages suivis sont conservés. Les tests de
|
||||
création/réouverture, le cas OFF et `check build assemblePack` passent.
|
||||
|
||||
## Alpha.20 — identité des bâtiments
|
||||
|
||||
La passe reprend les six bâtiments de ville, les quatorze infrastructures,
|
||||
les parcs et les sous-sols. Bois de chêne et de bouleau, granit, diorite et tuf
|
||||
identifient les usages ; biseaux, baies, corniches, portiques et verrières
|
||||
donnent du relief aux volumes. Les routes reçoivent des bordures de briques
|
||||
de tuf et des blocs ciselés. Les vitres et grilles se raccordent entre chunks.
|
||||
|
||||
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, avec **Structures
|
||||
expérimentales** activées pour retrouver le témoin. Les anciens mondes gardent
|
||||
leur génération. Le bouton natif **Personnaliser** et ses trois tailles restent
|
||||
identiques. Voir le [catalogue illustré](docs/architecture-alpha20.md) et les
|
||||
[essais alpha.20](docs/testing-alpha20.md). `check build assemblePack`, les essais natifs et les artefacts sont vérifiés ;
|
||||
la [release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
|
||||
et le canal packwiz sont publiés. La même instance Prism est synchronisée
|
||||
deux fois, avec 925 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Alpha.19 — anciennes infrastructures
|
||||
|
||||
Le chantier ajoute une première passe des quatorze lieux du
|
||||
[catalogue](docs/structures-audit.md), avec leurs accès, des petits butins vanilla
|
||||
et des voies de fret ou chemins abandonnés entre destinations réelles.
|
||||
L’option **Structures expérimentales** reste enregistrée à la création du monde.
|
||||
La nouvelle génération garde les anciennes sauvegardes intactes. Aucun système
|
||||
de progression, mailbox, caméra ou réacteur n’est activé.
|
||||
Les trois tailles sont vérifiées en génération native ; création et réouverture
|
||||
conservent les structures et les coffres. `check build assemblePack` passe.
|
||||
Pour voir le grand fret : **Moyen, graine 0**, option activée ;
|
||||
`/sanctuary structures` liste les entrées. Voir [les preuves et la carte](docs/testing-alpha19.md).
|
||||
La [release alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
|
||||
et le canal packwiz sont publiés. La même instance Prism est synchronisée deux
|
||||
fois, avec 767 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Alpha.17 — prototype des réseaux oubliés
|
||||
|
||||
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
|
||||
initiale : mines et ateliers, salles étranges, installations techniques
|
||||
abandonnées. Chaque réseau possède une entrée de surface et des salles reliées,
|
||||
avec mobilier, outils vanilla et traces de travaux interrompus. Les continents
|
||||
conservent leur génération urbaine sans recevoir ces réseaux supplémentaires.
|
||||
|
||||
Dans **Personnaliser**, l’option **Structures expérimentales** accompagne les
|
||||
trois tailles habituelles. Elle est enregistrée avec chaque nouveau monde ;
|
||||
elle permet de comparer un monde avec les nouveaux lieux et un autre avec le
|
||||
socle urbain seul. La présence des réseaux dépend des emplacements admissibles
|
||||
trouvés dans un budget borné. Les connexions entre familles restent à construire.
|
||||
|
||||
Le prototype demande un **nouveau monde**. Les mondes 16 et antérieurs gardent
|
||||
leur génération. Revenir au binaire 16 ne permet pas d’ouvrir un monde 17 : les
|
||||
mondes d’essai restent séparés. Voir [le contrat alpha.17](docs/testing-alpha17.md).
|
||||
Le réacteur, la progression et les reliques restent prévus pour la suite.
|
||||
|
||||
`check build assemblePack`, création/réouverture et option désactivée passent.
|
||||
Les empreintes natives de la génération 16 restent identiques aux témoins publiés.
|
||||
La [release alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
|
||||
et le canal packwiz sont publiés ; la même instance Prism est synchronisée deux
|
||||
fois, avec 767 fichiers personnels et réglages suivis conservés. Pour voir les
|
||||
trois réseaux, créer un monde **Moyen, graine 0**, option activée ;
|
||||
`/locate structure sanctuary:forgotten_networks` indique une entrée de surface.
|
||||
|
||||
## Alpha.16 — Sanctuary habitée avant nous
|
||||
|
||||
Une ville compacte apparaît sur la nouvelle île de départ lorsque les structures
|
||||
sont activées. Les étages varient leurs plans ; les sanitaires sont clos et les
|
||||
lits adossés aux murs. Sous la ville, la génération recherche une piscine couverte
|
||||
de **50 × 25 blocs**, des parkings de **3 à 8 plateformes**, une grande salle
|
||||
sèche et des égouts qui s’ouvrent vers l’extérieur avec de vrais écoulements.
|
||||
Chaque équipement dépend du relief et des appuis : les six îles testées ont une
|
||||
ville, mais seules trois accueillent la grande piscine. Les continents conservent
|
||||
leurs villes conditionnelles.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour la génération 16. Les mondes publiés
|
||||
jusqu’à l’alpha.15 gardent leur génération, y compris dans leurs futurs chunks.
|
||||
Le bouton natif **Personnaliser** conserve Petit 512, Moyen 724 par défaut et
|
||||
Grand 1 024. Le [protocole alpha.16](docs/testing-alpha16.md) donne les preuves,
|
||||
les limites et les coordonnées du monde **Moyen, graine 0**, qui contient la
|
||||
piscine, huit niveaux de parking, la salle et l’égout ouvert.
|
||||
|
||||
`check build assemblePack`, la création et la réouverture natives passent.
|
||||
La compatibilité 15 est vérifiée contre les empreintes de la version publiée.
|
||||
La [release alpha.16](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.16)
|
||||
est disponible ; deux synchronisations de la même instance Prism conservent
|
||||
les 767 fichiers personnels et réglages suivis.
|
||||
Les futurs donjons formeront plusieurs réseaux parfois connectés, avec des
|
||||
installations encore utilisables et des chantiers interrompus. Le réacteur,
|
||||
les reliques et leurs récompenses restent à construire.
|
||||
|
||||
## Alpha.15 — ville compacte et réseaux
|
||||
|
||||
Les bâtiments de Lost City reçoivent des plans intérieurs détaillés : pièces,
|
||||
portes, vestibules, couloirs, escaliers, chambres, cuisines et salles d’eau.
|
||||
Les égouts relient des salles et équipements souterrains, avec garages, pool
|
||||
rooms et accès depuis la surface. Les bâtiments se regroupent en une
|
||||
petite ville compacte avec rues, carrefours et entrées reliés. Les ponts et
|
||||
viaducs prolongent le réseau ; le terrain est dégagé dans l’emprise urbaine.
|
||||
Une grande salle souterraine prépare la future machine d’expansion.
|
||||
|
||||
Le biome Lost City est conservé car il pilote réellement les biomes urbains
|
||||
et leurs apparitions de zombies vanilla. La nouvelle génération **15** demande
|
||||
un **nouveau monde Sanctuary** ; les mondes 12/13/14 gardent leur génération.
|
||||
Les tailles et le bouton natif **Personnaliser** restent identiques. Le
|
||||
[protocole alpha.15](docs/testing-alpha15.md) consigne les preuves et les limites. Les tests de création et de réouverture
|
||||
passent, ainsi que `check build assemblePack`. La
|
||||
[release alpha.15](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.15)
|
||||
est publiée ; la même instance Prism est synchronisée deux fois, avec 767
|
||||
fichiers personnels et réglages suivis conservés. Créer un **nouveau monde**,
|
||||
y compris après le JAR d’essai anticipé15. La machine d’expansion et ses cartes
|
||||
préremplies restent prévues pour un prochain ticket.
|
||||
|
||||
## Alpha.14 — Lost City
|
||||
|
||||
Lost City est un biome de quartiers contemporains abandonnés, composé par du
|
||||
code Sanctuary original. Des poches urbaines peuvent apparaître sur l’île de
|
||||
départ ; le profil d’expansion `lost_city` permet d’en rechercher dans les
|
||||
nouveaux continents. Le relief et ses fondations décident des emplacements :
|
||||
un quartier ou un type de bâtiment n’est pas garanti sur chaque île.
|
||||
|
||||
Les 17 types comprennent rues, ponts, quatre parcs (boisé, jardin, aire de jeux,
|
||||
mémorial), places, marchés, parkings, excavations, piscines abandonnées,
|
||||
immeubles d’appartements, hôtels, bibliothèques, restaurants avec cuisine,
|
||||
casernes et halls. **Pierre, pierre lisse et briques de pierre** forment la
|
||||
palette principale, avec des variantes fissurées ou moussues, du verre et
|
||||
quelques détails de fer et de chêne noir. Les étages disposent d’escaliers et
|
||||
les pièces d’aménagements propres à leur usage.
|
||||
|
||||
Les parkings souterrains, fouilles et piscines demandent une épaisseur de roche
|
||||
vérifiée sur toute leur emprise. Les piscines ont un fond, des parois continues
|
||||
et des marches ; les ponts enjambent de courtes failles entre deux berges
|
||||
supportées. Les monstres naturels du biome sont des zombies vanilla, soumis
|
||||
aux règles de lumière et de difficulté du jeu. Aucun spawner ou zombie spécial
|
||||
n’est ajouté ; les variantes de zombies et la progression restent pour la bêta.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour la génération 14. Le bouton natif
|
||||
**Personnaliser** conserve Petit 512, Moyen 724 par défaut et Grand 1 024.
|
||||
Les mondes 12/13 gardent leurs codecs, journaux, paramètres et chunks.
|
||||
|
||||
Dans un nouveau monde de génération 14, les outils opérateur acceptent des
|
||||
expansions de 64, 128, 256, 512 ou 1 024 blocs. `quick` sans taille effectue un
|
||||
tirage reproductible favorisant les petites îles. Les emplacements suivent des
|
||||
secteurs directionnels autour du parent, avec séparation des emprises écrites.
|
||||
La reprise éventuelle de chunks de vide déjà explorés exige un certificat
|
||||
strict lié au journal et une réouverture du monde ; aucun terrain occupé n’est
|
||||
admis dans ce mécanisme.
|
||||
|
||||
Le [contrat et protocole alpha.14](docs/testing-alpha14.md) précisent les
|
||||
commandes, la protection des sauvegardes et les vérifications réussies :
|
||||
`check build assemblePack`, création et réouverture des quartiers, reprise du
|
||||
vide certifié et validation des artefacts de distribution.
|
||||
La [release alpha.14](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.14),
|
||||
le MRpack et le canal packwiz sont publiés. Deux installations isolées puis deux
|
||||
synchronisations de la même instance Prism Mac passent ; les 713 fichiers
|
||||
personnels et réglages suivis conservent leurs hashes. Aucun monde personnel
|
||||
n’a été ouvert ou régénéré. La validation native Windows reste à effectuer.
|
||||
|
||||
## Alpha.13.1 — correctif des expansions sous Windows
|
||||
|
||||
Le journal d’expansion n’essaie plus d’ouvrir son dossier comme un fichier sous
|
||||
Windows, ce qui provoquait `AccessDeniedException` et bloquait les expansions.
|
||||
La synchronisation du fichier, son remplacement atomique et sa relecture restent
|
||||
obligatoires. Le format du journal et la génération alpha.13 restent identiques.
|
||||
|
||||
Après installation du correctif, **rouvrir le même monde** : une réservation déjà
|
||||
enregistrée est reprise après vérification du journal. Il ne faut pas supprimer
|
||||
`sanctuary-world-v13` ni recréer le monde pour ce correctif. Voir le
|
||||
[dépannage Windows](docs/alpha13-worldgen.md) et la [validation](docs/testing.md).
|
||||
|
||||
La [release alpha.13.1](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.13.1)
|
||||
et le canal packwiz sont publiés. Deux synchronisations isolées et deux
|
||||
synchronisations de l’instance Mac existante passent ; les 713 fichiers personnels
|
||||
et réglages suivis restent identiques. Aucun accès à l’instance Windows n’a eu lieu.
|
||||
|
||||
## Alpha.13 — un seul monde Sanctuary
|
||||
|
||||
À la création d’un **nouveau monde**, sélectionner **Sanctuary**, puis utiliser
|
||||
le bouton natif **Personnaliser** pour choisir la taille de l’île :
|
||||
|
||||
| Taille | Diamètre nominal |
|
||||
| Version | Notes |
|
||||
| --- | --- |
|
||||
| Petit | 512 blocs |
|
||||
| Moyen, par défaut | 724 blocs |
|
||||
| Grand | 1 024 blocs |
|
||||
|
||||
**Terminé** conserve le choix ; **Annuler** revient sans l’appliquer. La hauteur
|
||||
reste de 384 blocs. Ces tailles décrivent une emprise, sans promettre un nombre
|
||||
de joueurs, une aire constructible ou un stock de minerais.
|
||||
|
||||
L’île reprend les forêts, le relief flottant et les plans d’eau complets du
|
||||
générateur précédent : bassins, rivière, terrasses et cascades restent liés
|
||||
aux possibilités naturelles du terrain. Les poches de cavités natives de Minecraft
|
||||
du laboratoire rejoignent cette île. Les commandes opérateur
|
||||
d’expansion, avec leur distance facultative et leur recherche d’emplacements
|
||||
libres, sont accessibles dans ce même nouveau monde. Les déblocages par la
|
||||
production et l’XP restent à construire.
|
||||
|
||||
Les anciens choix Petit/Grand et Laboratoire sont cachés à la création, tout
|
||||
en restant chargeables avec leurs paramètres enregistrés. **Aucun monde
|
||||
existant n’est converti ni régénéré.** Le [guide alpha.13](docs/alpha13-worldgen.md)
|
||||
détaille la création, les expansions et la séparation des sauvegardes.
|
||||
|
||||
`check build assemblePack`, les tests client, les trois tailles et les scénarios
|
||||
d’expansion/rechargement passent. Moyen atteint 63,66 secondes pour la création
|
||||
puis 289 chunks FULL dans le test serveur de référence. Les preuves et leurs
|
||||
limites sont dans [Validation](docs/testing.md).
|
||||
|
||||
À la livraison alpha.13, le canal packwiz, le MRpack et la même instance Prism
|
||||
avaient été mis à jour, avec les 601 fichiers personnels et réglages alors suivis conservés.
|
||||
Voir [Distribution](docs/packwiz.md).
|
||||
|
||||
## Socle livré jusqu’à l’alpha.8
|
||||
|
||||
- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz.
|
||||
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
|
||||
flottante finie, un terrain naturel et un extérieur vide.
|
||||
- Île tempérée aux teintes douces : forêts de chênes et de bouleaux, clairières
|
||||
fleuries et affleurements de pierre, andésite, diorite et granite. Des failles
|
||||
courbes ouvrent le terrain flottant issu du générateur 26.2. Les corniches
|
||||
humides accueillent chênes noirs et champignons ; les essences rares restent
|
||||
ponctuelles.
|
||||
- Géologie enrichie en profondeur : tuf, pierres moussues et boue compactée
|
||||
dans les intérieurs humides, puis ardoise des abîmes, roche noire, basalte et
|
||||
poches de soufre. Les volumes existants fournissent leur support.
|
||||
- Petits filons de charbon, fer, cuivre, or, redstone, lapis, diamant et émeraude
|
||||
à des altitudes adaptées au terrain. Les essais de charbon, fer et cuivre
|
||||
exposés sont plus fréquents. Les stocks se constatent après génération.
|
||||
- Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires
|
||||
et les choix ultérieurs de spawn ne sont pas remplacés.
|
||||
- Nouveaux mondes sur 384 blocs (`Y=0..383`), île relevée de 64 blocs,
|
||||
dessous sculpté et nuages au-dessus du terrain. Les anciennes sauvegardes
|
||||
conservent leurs paramètres de génération.
|
||||
- Étangs, petits lacs, plages plus larges et dépôts de
|
||||
trois à cinq blocs de sable, gravier, pierre ou terre enherbée. La canne à
|
||||
sucre apparaît lorsque les rives respectent ses règles vanilla.
|
||||
- Recherche de bassins plus grands et d’une longue rivière calme au tracé
|
||||
arrondi, avec largeur variable et berges progressives. La liaison de deux
|
||||
bassins, sa source et sa cascade entrante sont contrôlées dans les mondes
|
||||
de test de l’alpha.8.
|
||||
- Quelques sources dans des niches rocheuses naturelles, y compris aux étages
|
||||
inférieurs : Minecraft fait couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
|
||||
- Une petite poche de lave couverte est recherchée en priorité sur une corniche
|
||||
profonde, puis plus haut si nécessaire. Une à deux sources profondes
|
||||
supplémentaires peuvent former des coulées, vérifiées par les ticks du moteur.
|
||||
|
||||
Jusqu’à l’alpha.9, l’île a une emprise nominale de 512 blocs de diamètre, avec une bordure sculptée
|
||||
par des bruits liés à la seed. Aucune masse centrale n'est ajoutée pour forcer
|
||||
le terrain : le spawn cherche un emplacement naturel. Le catalogue
|
||||
TerraMix, les continents déverrouillables, les océans, les ruines et
|
||||
les autres fonctionnalités restent à construire par tickets.
|
||||
|
||||
La génération alpha.8, `sanctuary:sanctuary_rift`, passe les contrôles moteur
|
||||
sur les trois graines de référence, avec 1 800 ticks de fluides. Ces résultats
|
||||
ne valident pas par avance l’incrément suivant. Les observations et leurs
|
||||
limites figurent dans [Validation](docs/testing.md).
|
||||
|
||||
## Alpha.9, historique : cavités vivantes et terrasses d’eau
|
||||
|
||||
L’incrément ajoute des cavités luxuriantes et lumineuses sous l’île, des
|
||||
secteurs à spéléothèmes, des géodes d’améthyste et des poches de soufre moins
|
||||
envahissantes, avec de vrais geysers de soufre lorsque leurs conditions sont
|
||||
réunies. Les forêts de surface sont conservées ; la recherche de lacs plus
|
||||
profonds et volumineux accompagne des groupes facultatifs de bassins naturels
|
||||
étagés. Leurs cascades relient les paliers et permettent de circuler par la nage.
|
||||
L’image des « rizières » décrit ce paysage d’eau en terrasses : aucune culture
|
||||
de riz n’est ajoutée.
|
||||
|
||||
**La validation moteur alpha.9 passe sur les trois graines de référence.**
|
||||
Les preuves de terrasses, geysers et décor souterrain sont cumulées sur ces
|
||||
trois mondes, sans imposer chaque élément à chacun. La version est publiée
|
||||
et la même instance Prism est synchronisée ; voir [Distribution](docs/packwiz.md). Le
|
||||
nouveau réglage `sanctuary:sanctuary_cavern` nécessite de créer un **nouveau
|
||||
monde Sanctuary**. Les anciennes générations jusqu’à l’alpha.8 gardent leurs
|
||||
paramètres ; aucune sauvegarde n’est régénérée. Les cavités restent dans
|
||||
l’Overworld, sans créer la dimension des cavernes prévue par la vision.
|
||||
Dappled Forest reste réservé aux futurs continents et présent dans les
|
||||
anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
|
||||
de ressources n’est garanti sur chaque graine. Voir le ticket local
|
||||
[WG-10](docs/backlog.md).
|
||||
|
||||
## Alpha.10, historique : îles pour 5, 20 ou 100 joueurs
|
||||
|
||||
L’alpha.10 proposait trois tailles, avec **20 joueurs par défaut**.
|
||||
Leur aire nominale est proportionnelle à la capacité choisie, sur la base
|
||||
de l’île de 512 blocs pour cinq joueurs. La hauteur reste de 384 blocs.
|
||||
|
||||
| Choix à la création | Identifiant du preset | Capacité | Diamètre nominal |
|
||||
| --- | --- | --- | --- |
|
||||
| Sanctuary 5 joueurs | `sanctuary:sanctuary_5` | 5 | 512 blocs |
|
||||
| Sanctuary, choix par défaut | `sanctuary:sanctuary` | 20 | 1 024 blocs |
|
||||
| Sanctuary 100 joueurs | `sanctuary:sanctuary_100` | 100 | Environ 2 290 blocs |
|
||||
|
||||
Ces capacités dimensionnent l’île ; elles ne fixent pas la limite de
|
||||
connexions du serveur. Le contour réel dépend du relief et de la graine,
|
||||
et le stock de ressources reste celui de la génération. La taille se choisit
|
||||
pour un **nouveau monde** : aucune ancienne île n’est agrandie et aucune
|
||||
sauvegarde n’est régénérée lors de la mise à jour.
|
||||
Les paramètres sauvegardés sont distincts : `sanctuary:population_5`,
|
||||
`sanctuary:population_20` et `sanctuary:population_100`.
|
||||
|
||||
**Les trois profils passent les onze tests moteur sur la graine `0`, avec
|
||||
1 800 ticks réels de fluides.** Le choix de capacité persiste après écriture
|
||||
et relecture sur disque. La vérification conserve 212 anciens fichiers de
|
||||
production strictement identiques ; huit fichiers d’intégration ou d’interface
|
||||
accueillent les nouveaux choix. La première planification locale reste coûteuse,
|
||||
environ 83–88 secondes pour les premiers essais 5/20 sur cette configuration.
|
||||
La génération en masse reste à optimiser. Ces essais ne simulent pas
|
||||
100 connexions et ne donnent aucune garantie de performances multijoueurs.
|
||||
Les diamètres nominaux ne permettent pas d’extrapoler les stocks ou l’aire
|
||||
réellement habitable. La version est publiée et la même instance Prism a été
|
||||
synchronisée deux fois, avec 363 fichiers personnels et réglages suivis conservés.
|
||||
Voir [WG-11](docs/backlog.md), [Validation](docs/testing.md) et
|
||||
[Distribution](docs/packwiz.md).
|
||||
|
||||
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
|
||||
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
|
||||
développement relève les ressources après génération ; le système d'expansion
|
||||
en jeu reste à construire. Voir [Expansion et ressources](docs/expansion.md).
|
||||
|
||||
Les détails du terrain et de ses limites sont dans [Génération](docs/worldgen.md),
|
||||
et la provenance dans [Audit 26.2](docs/migration-26.2.md).
|
||||
|
||||
## Alpha.11, historique : Petit, Grand et génération plus rapide
|
||||
|
||||
Les anciens formats sont trop grands et éloignent les constructions. Le
|
||||
nouveau choix de départ se limite à **Petit** et **Grand**, avec **Grand par
|
||||
défaut**. Petit garde un diamètre nominal de 512 blocs ; Grand mesure environ
|
||||
724 blocs, soit deux fois l’aire nominale de Petit. La hauteur reste de
|
||||
384 blocs. Les repères internes 5 et 10 servent au calcul de surface : ils
|
||||
ne sont ni les noms des choix ni une promesse de capacité multijoueur.
|
||||
|
||||
Grand utilise le nouveau réglage `sanctuary:population_10`. Les anciennes
|
||||
parties `population_5`, `population_20` et `population_100` gardent leurs
|
||||
paramètres ; les formats 20/100 sont retirés des choix publics de création,
|
||||
sans réduire les mondes déjà enregistrés. Aucun monde personnel n’est ouvert
|
||||
pour les essais.
|
||||
|
||||
Le ticket [WG-12](docs/backlog.md) réduit les calculs répétés de terrain et
|
||||
permet de lire un plan régional déjà prêt pendant le calcul du suivant.
|
||||
À géométrie 20 identique, la création puis 289 chunks autour du spawn passent
|
||||
de 407,43 à 62,54 secondes (−84,65 %) ; 80 chunks d’exploration passent de
|
||||
103,14 à 14,05 secondes. Le nouveau Grand atteint 62,67 secondes sur le même
|
||||
protocole de départ. Ces essais serveur sur une graine et une machine ne
|
||||
mesurent pas le rendu du client ni une charge multijoueur.
|
||||
|
||||
`check build assemblePack` passe, ainsi que douze tests moteur pour chacun
|
||||
des deux choix, avec 1 800 ticks de fluides. Plus de trois millions de
|
||||
comparaisons de densité confirment l’équivalence de calcul. Les mesures et
|
||||
limites sont dans [Validation](docs/testing.md) ; publication et synchronisation
|
||||
de la même instance sont consignées dans [Distribution](docs/packwiz.md).
|
||||
L’alpha.11 est publiée ; la même instance Prism a été synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Alpha.12, historique — laboratoire d’expansion
|
||||
|
||||
Un troisième preset **Sanctuary — Laboratoire** permet de tester les expansions
|
||||
dans un **nouveau monde**, sur la même version Minecraft. Les choix Petit et
|
||||
Grand gardent leur génération alpha.11. Le laboratoire possède sa propre île
|
||||
initiale de 512 blocs, avec un relief supérieur légèrement déformé et des biomes
|
||||
vanilla. Les nouvelles îles choisissent séparément leur parent, direction,
|
||||
climat, diamètre nominal (64/128/256/512) et relief (naturel, pics, canyon, volcan).
|
||||
|
||||
Avec les commandes autorisées ou le statut opérateur :
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick north autumn 128 peaks
|
||||
/sanctuary expansion list
|
||||
/sanctuary expansion visit island_1
|
||||
/sanctuary expansion create oasis island_1 east arid 64 natural
|
||||
```
|
||||
|
||||
Le premier exemple ouvre une forêt automnale au nord ; le second continent
|
||||
part vers l’est depuis cette île. `quick north` seul emploie la convention froide,
|
||||
avec 128 blocs et un relief naturel. `create` permet de choisir explicitement un
|
||||
identifiant et un parent. `preview` propose l’emplacement sans l’activer ;
|
||||
`resume <id>` reprend une réservation interrompue. L’autocomplétion liste les
|
||||
directions, douze profils climatiques et quatre reliefs.
|
||||
|
||||
Les anciens chunks, même vides, sont refusés dans l’emprise d’une nouvelle île.
|
||||
Le registre persiste côté serveur ; la préparation est progressive et la visite
|
||||
attend un terrain prêt. Villages, temples, cabanes de marais et camps abandonnés
|
||||
vanilla peuvent être retenus selon leur biome et leurs fondations. Les autres
|
||||
structures, la grande hydrologie des continents, les recherches, les boîtes de
|
||||
dépôt et le refuge Void restent des étapes suivantes. Un biome humide ne promet
|
||||
pas encore un lagon ou un océan. Les règles, limites et essais figurent dans le
|
||||
[guide du laboratoire](docs/alpha12-laboratory.md).
|
||||
|
||||
`check build assemblePack` et les essais d’expansion/rechargement de graine 42
|
||||
passent. L’alpha.12, son MRpack et le canal packwiz sont publiés ; la même
|
||||
instance Prism a été synchronisée deux fois, avec les 466 fichiers personnels
|
||||
et réglages suivis conservés. Voir [Distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.12.1, historique — distance et emplacements libres
|
||||
|
||||
Le correctif ajoute une distance minimale facultative à la fin des commandes :
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick north autumn 128 peaks 2048
|
||||
/sanctuary expansion create desert sanctuary east arid 128 natural 4096
|
||||
```
|
||||
|
||||
La distance se mesure entre les centres depuis le parent choisi. Sans distance,
|
||||
le placement reste automatique. Les emplacements déjà générés sont maintenant
|
||||
écartés en cherchant plus loin dans la même direction, sans changer le climat
|
||||
ni réécrire les chunks. Les mondes laboratoire alpha.12 peuvent être conservés :
|
||||
le journal et la génération restent compatibles. Voir le [guide](docs/alpha12-laboratory.md).
|
||||
|
||||
`check build assemblePack`, les scénarios moteur et les tests de distance passent.
|
||||
L’alpha.12.1 et son MRpack sont publiés ; la même instance Prism est synchronisée
|
||||
deux fois, avec 466 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Versions et prérequis
|
||||
|
||||
Au 8 septembre 2026, la cible disponible est **26.3-pre-2**. Le dépôt ne prétend
|
||||
pas cibler une version finale 26.3 déjà sortie. Le passage à la version finale
|
||||
sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
|
||||
| Composant | Version fixée |
|
||||
| --- | --- |
|
||||
| Minecraft Java | 26.3-pre-2 |
|
||||
| Java JDK | 25 |
|
||||
| Fabric Loader | 0.19.5 |
|
||||
| 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.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
|
||||
3.11–3.14 si `python3` désigne l'ancien Python fourni par macOS. Gradle est fourni
|
||||
par le wrapper. `packwiz` est utile pour modifier les dépendances ou servir le
|
||||
pack ; il n'est pas nécessaire pour le construire.
|
||||
|
||||
Références vérifiées : [Minecraft 26.3-pre-2](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-2),
|
||||
[Fabric](https://fabricmc.net/develop/),
|
||||
[version Fabric API](https://modrinth.com/mod/fabric-api/version/o9uChmGq).
|
||||
|
||||
## Construire et lancer
|
||||
|
||||
```sh
|
||||
git clone https://git.botsu.net/koka/sanctuary-beta.git
|
||||
cd sanctuary-beta
|
||||
./gradlew check build assemblePack
|
||||
```
|
||||
|
||||
`check` inclut les tests de forme et le serveur de test automatisé de Fabric,
|
||||
qui charge par défaut le nouveau générateur unifié Sanctuary dans un monde
|
||||
de développement neuf. Les suites historiques utilisent explicitement
|
||||
`-PsanctuaryUnifiedTests=false`.
|
||||
Les scénarios couverts et les essais multijoueurs restant à effectuer sont
|
||||
décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `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).
|
||||
|
||||
Le pack peut aussi être exporté en `.mrpack` avec packwiz pour une importation
|
||||
dans un lanceur compatible ; la commande est indiquée dans ce même guide.
|
||||
|
||||
Pour les essais réguliers, la même instance Prism suit un canal packwiz stable
|
||||
et se synchronise avant chaque lancement. La procédure de publication et de
|
||||
raccordement est dans [Distribution Prism](docs/packwiz.md).
|
||||
|
||||
Lancer le client de développement :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClient
|
||||
```
|
||||
|
||||
Créer un **nouveau monde**, sélectionner le type **Sanctuary**, puis
|
||||
**Personnaliser** pour changer la taille Moyen retenue par défaut.
|
||||
L'installation du mod seule ne transforme pas un monde Minecraft ordinaire.
|
||||
|
||||
Pour un serveur de développement, lancer `./gradlew :sanctuary:runServer`, puis
|
||||
suivre les indications du jeu concernant son EULA. Utiliser un dossier de monde
|
||||
neuf et choisir dans `mods/sanctuary/run/server.properties` :
|
||||
|
||||
```properties
|
||||
level-type=sanctuary:sanctuary
|
||||
level-name=sanctuary-dev
|
||||
level-seed=42
|
||||
```
|
||||
|
||||
Relancer ensuite la même commande. Ne pas remplacer le générateur d'une
|
||||
sauvegarde 26.2 : aucun outil de migration de ses chunks n'est livré ici.
|
||||
|
||||
## Organisation
|
||||
|
||||
| Chemin | Responsabilité |
|
||||
| --- | --- |
|
||||
| `mods/sanctuary/` | Code, ressources et tests du mod Sanctuary |
|
||||
| `packwiz/` | Manifeste source, dépendances distantes et hashes |
|
||||
| `ressources-pack/` | Sources graphiques existantes et futurs resource packs |
|
||||
| `shaders-pack/` | Sources et réglages de shaders à venir |
|
||||
| `docs/` | Vision, backlog, migration, génération et validation |
|
||||
| `.gitea/ISSUE_TEMPLATE/` | Modèles de tickets fonctionnalités et bugs |
|
||||
| `.gitea/workflows/` | Vérification sur un runner Gitea compatible |
|
||||
|
||||
It's Alive !, Only Fun et Master Key restent des modules autonomes prévus par la
|
||||
vision. Leurs sources 26.2 ne sont pas copiées dans ce socle. Fabric API est la
|
||||
seule dépendance de gameplay actuellement distribuée avec le mod.
|
||||
|
||||
## Travailler par tickets
|
||||
|
||||
Un ticket décrit un comportement à obtenir ou un bug à reproduire. On livre
|
||||
un petit incrément, on le vérifie en jeu si nécessaire, puis on ajuste avec
|
||||
le ticket suivant. Pour le terrain, joindre la seed et les coordonnées aide
|
||||
à retrouver exactement le problème. Voir [Contribuer](CONTRIBUTING.md).
|
||||
|
||||
Le [backlog initial](docs/backlog.md) contient des propositions locales, pas des
|
||||
issues déjà publiées. Le prochain travail après ce socle est le réglage de
|
||||
l'île, puis le placement de continents et leur ouverture sans écraser les
|
||||
constructions des joueurs.
|
||||
|
||||
## Licence et crédits
|
||||
|
||||
Sanctuary conserve la licence **GPL-3.0-or-later** du projet 26.2. Voir
|
||||
[LICENSE](LICENSE) et [provenance et crédits](THIRD_PARTY_NOTICES.md).
|
||||
Projet communautaire indépendant de Mojang Studios et Microsoft.
|
||||
| beta.001 | [Les quatre anciennes expéditions](notes/beta.001.md) |
|
||||
| beta.002 | [Biomes boréaux, corail et temples](notes/beta.002.md) |
|
||||
| beta.003 | [Créer son habitant et commencer la progression](notes/beta.003.md) |
|
||||
| beta.004 | [Cadre Blocodex et sprint](notes/beta.004.md) |
|
||||
| beta.005 | [Carte native et noms de couleurs](notes/beta.005.md) |
|
||||
| beta.006 | [Brouillard, grille et Demeure](notes/beta.006.md) |
|
||||
| beta.007 | [Carte immersive](notes/beta.007.md) |
|
||||
| beta.008 | [Minage et vein mining](notes/beta.008.md) |
|
||||
| beta.009 | [Construction et vein building](notes/beta.009.md) |
|
||||
| beta.010 | [Inventaire jusqu'à six rangées](notes/beta.010.md) |
|
||||
| beta.011 | [Factions, prestige et New Game+](notes/beta.011.md) |
|
||||
| beta.012 | [Inventaire et progression lisible](notes/beta.012.md) |
|
||||
| beta.013 | [Progression, prestige et repères visuels](notes/beta.013.md) |
|
||||
| beta.014 | [Rangement, hotbar mobile et préfixe de faction](notes/beta.014.md) |
|
||||
| beta.015 | [Icônes de progression communes](notes/beta.015.md) |
|
||||
| beta.016 | [Les premiers gestes rapportent de l’XP](notes/beta.016.md) |
|
||||
| beta.017 | [Nage, S’allonger, Se reposer et fabrication manuelle](notes/beta.017.md) |
|
||||
| beta.018 | [Familiers, cape et cosmétique de tête](notes/beta.018.md) |
|
||||
| beta.019 | [Pouvoirs et déplacements des familiers](notes/beta.019.md) |
|
||||
| beta.020 | [Œufs singuliers, portage et plongeon](notes/beta.020.md) |
|
||||
| beta.022 | [Création de monde plus rapide](notes/beta.022.md) |
|
||||
| beta.023 | [Cosmétiques directement sur leur case](notes/beta.023.md) |
|
||||
| beta.024 | [Recettes progressives dans le Blocodex](notes/beta.024.md) |
|
||||
| beta.025 | [Barres d’XP des compétences](notes/beta.025.md) |
|
||||
| beta.026 | [Cache d’affichage Demeure sur la carte](notes/beta.026.md) |
|
||||
| beta.027 | [Piles de joueurs](notes/beta.027.md) |
|
||||
| beta.028 | [Plongeon jusqu’à collision](notes/beta.028.md) |
|
||||
| beta.029 | [Cri et recul des familiers](notes/beta.029.md) |
|
||||
| beta.030 | [Lumières portées et objets jetés](notes/beta.030.md) |
|
||||
| beta.031 | [Apparition galactique](notes/beta.031.md) |
|
||||
| beta.032 | [Refonte des 88 familiers](notes/beta.032.md) |
|
||||
| beta.033 | [Menus directs et profil de test rapide](notes/beta.033.md) |
|
||||
| beta.034 | [Fil de découvertes et hotbar agrandie](notes/beta.034.md) |
|
||||
| beta.035 | [Collections Minecraft](notes/beta.035.md) |
|
||||
| beta.036 | [Notifications et suivi choisi](notes/beta.036.md) |
|
||||
| beta.037 | [Temps réel et suivi commun](notes/beta.037.md) |
|
||||
| beta.038 | [Nourriture, têtes-tombes et factions](notes/beta.038.md) |
|
||||
| beta.039 | [Hello World, savoir alimentaire et inventaires de mort](notes/beta.039.md) |
|
||||
| beta.040 | [Mode opérateur, fiche Habitant et ciel partagé](notes/beta.040.md) |
|
||||
| beta.041 | [Portage des bébés](notes/beta.041.md) |
|
||||
| beta.042 | [Joueurs assis sur la tête](notes/beta.042.md) |
|
||||
| beta.043 | [Blocs portés et icône officielle](notes/beta.043.md) |
|
||||
| beta.044 | [Pouvoirs natifs des familiers](notes/beta.044.md) |
|
||||
| beta.045 | [Menu principal](notes/beta.045.md) |
|
||||
| beta.046 | [Constellations, vol et zoom](notes/beta.046.md) |
|
||||
| beta.047 | [Étoiles à la longue-vue et boutons](notes/beta.047.md) |
|
||||
| beta.048 | [Bateaux collectifs et moteurs animaux](notes/beta.048.md) |
|
||||
| beta.049 | [Bateaux rectangulaires et chapeaux actifs](notes/beta.049.md) |
|
||||
| beta.050 | [Bateaux compacts et redstone portée](notes/beta.050.md) |
|
||||
| beta.051 | [Distributeur porté orienté par le regard](notes/beta.051.md) |
|
||||
| beta.052 | [Nom du familier de départ](notes/beta.052.md) |
|
||||
| beta.053 | [Brume progressive et saturation](notes/beta.053.md) |
|
||||
| beta.054 | [Duels, mises et niveaux des familiers](notes/beta.054.md) |
|
||||
| beta.055 | [Force et portage de joueurs](notes/beta.055.md) |
|
||||
| beta.056 | [Étoiles dispersées et saturation](notes/beta.056.md) |
|
||||
| beta.057 | [Météo quotidienne et nouvelles ambiances](notes/beta.057.md) |
|
||||
| beta.058 | [Altitude de la carte](notes/beta.058.md) |
|
||||
| beta.059 | [Bannières et cartes au trésor](notes/beta.059.md) |
|
||||
| beta.060 | [Objets et blocs sur les mobs](notes/beta.060.md) |
|
||||
| beta.061 | [Musique d’arrivée et montures familières](notes/beta.061.md) |
|
||||
| beta.062 | [Soleil et chapeaux des familiers](notes/beta.062.md) |
|
||||
| beta.063 | [Chargement et règle de jeu Sanctuary](notes/beta.063.md) |
|
||||
| beta.064 | [Cache et étapes réelles de préparation](notes/beta.064.md) |
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Provenance et crédits
|
||||
|
||||
Le code Sanctuary est distribué sous GPL-3.0-or-later, comme la version 26.2 dont
|
||||
la génération est reprise. Voir `LICENSE` et `docs/migration-26.2.md` pour la
|
||||
provenance détaillée et le périmètre du portage.
|
||||
|
||||
Minecraft appartient à Mojang Studios / Microsoft. Sanctuary est un projet
|
||||
communautaire indépendant. Le dépôt ne redistribue pas le jeu.
|
||||
|
||||
Les biomes de départ reprennent les configurations de forêt, plaine, forêt de
|
||||
bouleaux, forêt fleurie et Dappled Forest de Minecraft 26.3-pre-2, adaptées à
|
||||
une île finie. Les variantes alpha.6 réutilisent les végétations vanilla pour
|
||||
les prairies fleuries et les bosquets ; Dappled Forest reste disponible pour
|
||||
les sauvegardes alpha.5. L’alpha.7 réemploie les arbres vanilla (dont chêne noir,
|
||||
cerisier et essences rares), les champignons, le bambou et les blocs de soufre
|
||||
de cette version, avec des conditions propres à l’île. Les règles des petits minerais sont adaptées à partir des
|
||||
features vanilla de cette même version. Les références de blocs, végétation et
|
||||
minerais sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus.
|
||||
|
||||
Fabric Loader : Apache-2.0. Fabric API : Apache-2.0. Fabric Loom et le wrapper
|
||||
Gradle : Apache-2.0. Ces projets conservent leurs auteurs, notices et licences.
|
||||
Les dépendances sont résolues depuis leurs distributions officielles.
|
||||
|
||||
Le fichier préexistant `ressources-pack/helloworld/assets/minecraft_title.png`
|
||||
est conservé comme source graphique fournie par le propriétaire du dépôt.
|
||||
Il n'est pas encore installé dans le pack généré. Sa provenance et son adaptation
|
||||
à l'écran titre 26.3 seront précisées dans le ticket d'identité visuelle.
|
||||
|
||||
Les resource packs, shaders et mods communautaires envisagés dans la vision
|
||||
ne sont pas inclus automatiquement. Chaque ajout aura une version, une source,
|
||||
un hash et les crédits de sa distribution.
|
||||
@@ -1,20 +0,0 @@
|
||||
plugins {
|
||||
id 'base'
|
||||
id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false
|
||||
}
|
||||
|
||||
tasks.named('build') { dependsOn(':sanctuary:build') }
|
||||
tasks.named('check') { dependsOn(':sanctuary:check', 'verifyPack') }
|
||||
|
||||
tasks.register('verifyPack', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Verify pinned pack dependencies, versions and index hashes.'
|
||||
commandLine('python3', 'scripts/pack.py', 'check')
|
||||
}
|
||||
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', 'verifyPack')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
# Alpha.12 — laboratoire d’expansion
|
||||
|
||||
Ticket local **WG-13**, branche `codex/expansion-lab`. Contrat du laboratoire et guide
|
||||
d’essai. Cette livraison reste une alpha ; le canal distribué est suivi séparément
|
||||
dans [Distribution](packwiz.md).
|
||||
|
||||
## Décisions de conception
|
||||
|
||||
La direction géographique et le climat deviennent deux choix distincts. La
|
||||
boussole nord froid / sud chaud / ouest sec / est humide reste un raccourci
|
||||
facultatif. Une île peut prolonger le climat de son parent ou ouvrir une branche
|
||||
vers un autre climat. Les parents et les coordonnées forment un graphe persistant,
|
||||
permettant plusieurs couronnes autour de Sanctuary. Les recherches, leurs coûts,
|
||||
les boîtes aux lettres et les boîtes de dépôt sont planifiés pour plus tard.
|
||||
|
||||
Cette alpha fournit des commandes opérateur : créer, suivre, lister et visiter
|
||||
les expansions. Les tailles nominales des nouvelles îles sont 64, 128, 256 et
|
||||
512 blocs. Les reliefs de laboratoire s’appliquent aussi à l’île initiale, dans
|
||||
un nouveau preset expérimental. Les structures vanilla compatibles sont admises
|
||||
après contrôle de leur emprise et de leur support. Une absence reste possible.
|
||||
|
||||
L’alpha sert à éprouver les outils et les paysages. Le passage en bêta se fera
|
||||
après définition du périmètre de la vision et de la progression jouable ; ce
|
||||
numéro ne désigne pas une progression collective déjà livrée.
|
||||
|
||||
## Contrat de sauvegarde et d’ouverture v1
|
||||
|
||||
- Activation exclusivement dans un **nouveau monde du laboratoire alpha.12**.
|
||||
Les anciens presets, leurs paramètres, leurs chunks et leurs sauvegardes ne
|
||||
sont pas convertis. Il n’existe pas de commande de migration ou de régénération.
|
||||
- Un journal serveur propre au monde enregistre graine, version, origine,
|
||||
identifiant, parent, direction, climat, relief, diamètre, centre et statut.
|
||||
Les paramètres d’une île sont immuables après réservation.
|
||||
- Toute la réservation inclut terrain, décorations et halo moteur. Les régions
|
||||
déjà présentes sur disque, chargées ou en cours de déchargement sont refusées,
|
||||
même si leurs blocs sont tous vides. Une erreur de lecture interdit l’ouverture.
|
||||
- Le journal est écrit atomiquement et relu avant de publier le nouveau plan au
|
||||
générateur. Un journal existant absent ou corrompu n’est jamais remis à zéro.
|
||||
- La génération travaille uniquement dans la région nouvelle. Les chunks voisins
|
||||
nécessaires au moteur restent dans son halo. Aucun bloc d’une ancienne région
|
||||
n’est remplacé. Les références et le placement des structures utilisent le moteur.
|
||||
- Une réservation interrompue conserve son identité et sa graine. La préparation
|
||||
peut reprendre, sans double île ni nouveau tirage, jusqu’au statut prêt.
|
||||
- Les commandes exigent la permission opérateur. Être en créatif seul ne confère
|
||||
aucun droit sur le serveur. Une opération ne téléporte personne implicitement.
|
||||
- Le refuge Void et le déplacement collectif des joueurs restent un système
|
||||
distinct à concevoir. Ils ne servent pas de mécanisme de protection des chunks.
|
||||
|
||||
Le halo impose une distance supplémentaire dans cette première version. Le placer
|
||||
au plus proche ne permet pas de réutiliser silencieusement du vide déjà généré.
|
||||
Les points d’origine enregistrés ne suivent pas les changements de lit ou de spawn.
|
||||
|
||||
## Commandes et palette
|
||||
|
||||
Créer un **nouveau monde**, type **Sanctuary — Laboratoire**, avec les commandes
|
||||
activées. Pour un serveur neuf, utiliser `level-type=sanctuary:expansion_lab`
|
||||
et un nouveau `level-name`. La graine `42` fournit un cas reproductible observé
|
||||
avec un camp abandonné ; aucune structure n’est garantie sur chaque graine.
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick north autumn 128 peaks
|
||||
/sanctuary expansion list
|
||||
/sanctuary expansion visit island_1
|
||||
/sanctuary expansion create oasis island_1 east arid 64 natural
|
||||
/sanctuary expansion preview glacier sanctuary south glacial 256 canyon
|
||||
```
|
||||
|
||||
`quick` part toujours de Sanctuary et attribue un identifiant `island_N`.
|
||||
`create <id> <parent> <direction> <climate> <diameter> <relief> [distance]` ouvre une branche
|
||||
nommée depuis n’importe quelle île prête. `preview` emploie les mêmes paramètres ;
|
||||
il propose des coordonnées mais ne valide ni l’occupation des chunks ni le sol.
|
||||
Répéter un `create` identique rend la même réservation ; un autre profil sous
|
||||
le même identifiant est refusé. `resume <id>` reprend une préparation interrompue.
|
||||
Après une erreur du journal, il faut rétablir le stockage puis recharger le monde.
|
||||
|
||||
Depuis l’**alpha.12.1**, `quick`, `create` et `preview` acceptent une distance
|
||||
minimale facultative à la fin, de 0 à 100 000 blocs. Elle mesure la distance
|
||||
entre les centres du parent et de l’enfant, y compris en diagonale. L’alignement
|
||||
des chunks et les marges du terrain peuvent éloigner légèrement le résultat.
|
||||
Une valeur de 0, ou son omission, laisse le placement automatique.
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick north autumn 128 peaks 2048
|
||||
/sanctuary expansion create desert sanctuary east arid 128 natural 4096
|
||||
/sanctuary expansion preview glacier sanctuary northeast glacial 256 canyon 2048
|
||||
```
|
||||
|
||||
Si une zone est déjà générée, `create` et `quick` essaient plus loin dans la
|
||||
même direction, sur 32 positions maximum avec un budget total de lecture de
|
||||
30 secondes. Le climat, le relief et la graine restent ceux de la demande.
|
||||
`preview` propose seulement le premier emplacement géométrique ; le résultat
|
||||
réel peut être plus loin. Les erreurs de lecture interrompent la recherche.
|
||||
Les anciens appels restent valides. Un identifiant déjà créé garde sa position ;
|
||||
demander sous le même nom une distance supérieure à sa distance réelle est refusé.
|
||||
|
||||
Cette correction utilise le même journal v1 et les mêmes descripteurs de
|
||||
génération 12 : le monde laboratoire alpha.12 actuel peut être conservé. Aucun
|
||||
ancien chunk n’est régénéré ; la recherche réserve uniquement une nouvelle zone.
|
||||
|
||||
| Profil de commande | Surface recherchée |
|
||||
| --- | --- |
|
||||
| `temperate` | Chênes, bouleaux, clairières et fleurs |
|
||||
| `boreal` | Taïga, bosquets et neige en hauteur |
|
||||
| `glacial` | Plaines enneigées, pics gelés et pointes de glace |
|
||||
| `cold_humid` | Vieilles taïgas et bosquets froids |
|
||||
| `humid` | Forêt sombre, marécages et Pale Garden ponctuel |
|
||||
| `tropical` | Jungle, bambous et mangrove basse |
|
||||
| `warm` | Savane et plateaux |
|
||||
| `arid` | Désert, badlands et cheminées érodées |
|
||||
| `dry` | Prairies exposées et reliefs pierreux |
|
||||
| `cold_dry` | Neige, gravier et sommets gelés |
|
||||
| `autumn` | Dappled Forest, prairies et bouleaux ponctuels |
|
||||
| `cherry` | Cerisiers, prairies et bosquets en hauteur |
|
||||
|
||||
La sélection dépend de la graine, de l’altitude et de variations locales ; une
|
||||
petite île n’affiche pas nécessairement tous les biomes de son profil. Des poches
|
||||
luxuriantes, à spéléothèmes ou soufrées diversifient les étages inférieurs.
|
||||
Les reliefs `natural`, `peaks`, `canyon` et `volcano` sont indépendants du climat.
|
||||
Le volcan déforme et creuse la matière existante ; il ne garantit pas un cône
|
||||
plein ni un lac de lave lorsque la géométrie centrale est ajourée.
|
||||
|
||||
Les valeurs par défaut de `quick` suivent la boussole décrite dans
|
||||
[Expansion](expansion.md), avec diamètre 128 et relief naturel. Les directions
|
||||
acceptées sont `north`, `northeast`, `east`, `southeast`, `south`, `southwest`,
|
||||
`west`, `northwest`. L’autocomplétion fournit les paramètres disponibles.
|
||||
|
||||
## Portée technique et limites
|
||||
|
||||
Le codec `sanctuary:expansion_lab` enregistre le générateur ; le journal conserve
|
||||
les descripteurs de génération **12**. Chaque île emploie sa propre graine dérivée
|
||||
de la graine du monde et de son identifiant. Changer d’identifiant change donc
|
||||
le tirage. Les instances de bruit sont propres aux îles, y compris après reload.
|
||||
Les 34 biomes vanilla sont déclarés avant la première expansion.
|
||||
|
||||
La densité flottante existante sert de base. Le laboratoire ajoute les
|
||||
transformations des reliefs hauts et utilise les matériaux et décorations natifs.
|
||||
Ses carvers vanilla sont désactivés pour conserver les supports prospectés.
|
||||
Les grandes rivières et terrasses hydrologiques alpha.11 ne sont pas transférées
|
||||
dans ce prototype : les anciens presets les conservent. Le laboratoire essaie
|
||||
seulement de petites sources facultatives sur support rocheux fermé.
|
||||
|
||||
Les structures autorisées sont les villages (diamètre au moins 256), pyramides
|
||||
du désert et de jungle, cabanes de marais et camps abandonnés. Les starts natifs
|
||||
sont acceptés après vérification de leur emprise et de leurs fondations ; le
|
||||
moteur place les pièces, références et butins. Leur grille de sélection diffère
|
||||
de celle de `/locate` vanilla : cette commande n’est pas un outil fiable pour
|
||||
les retrouver dans le laboratoire. La preuve actuelle porte sur un camp ; elle
|
||||
ne valide pas visuellement chaque type de village ou de temple.
|
||||
|
||||
La réservation ajoute 32 blocs de décoration et 176 blocs de halo moteur autour
|
||||
de l’enveloppe de terrain. Les petites îles sont donc encore assez éloignées :
|
||||
le test de diamètre 64 place une première île à 800 blocs du centre de Sanctuary,
|
||||
puis une branche à 544 blocs de son parent. Un emplacement déjà exploré est
|
||||
écarté ; depuis l’alpha.12.1, la recherche continue plus loin sans réécrire ce vide.
|
||||
Il existe une limite de 64 régions, origine comprise, et de ±100 000 blocs pour
|
||||
leurs centres. La vérification initiale d’occupation est synchrone et peut
|
||||
suspendre brièvement le serveur pendant les lectures disque ; la préparation
|
||||
suivante avance chunk par chunk.
|
||||
|
||||
Les bateaux volants, montgolfières, panthéons, îles uniques en béton coloré,
|
||||
océans coralliens, recherches, coûts et boîtes de dépôt restent dans la vision.
|
||||
Aucun command block, transfert collectif au Void ou reset global n’est nécessaire
|
||||
pour utiliser ces commandes.
|
||||
|
||||
## Observations reproductibles
|
||||
|
||||
Sur la graine **42**, le scénario moteur a ouvert une île tropicale de 64 blocs
|
||||
au nord, puis une île aride de 64 blocs à l’est de celle-ci. Chaque préparation
|
||||
a atteint 100 chunks FULL. Les biomes sauvegardés, les points d’arrivée sur sol
|
||||
réel, les permissions du dispatcher, le refus des doublons incohérents, la
|
||||
conservation d’un bloc témoin sur Sanctuary et le refus du vide déjà chargé
|
||||
ont été vérifiés. Un camp abandonné possède un tonneau placé avec sa table de
|
||||
butin vanilla. Sur la graine **0**, ces expansions fonctionnent mais aucun des
|
||||
11 sites de structure candidats n’a passé les conditions de terrain ; le test
|
||||
exigeant une preuve de butin a donc échoué sur cette graine.
|
||||
|
||||
Un second processus Java a rouvert le monde de développement de graine 42 et
|
||||
retrouvé les trois régions prêtes, le bloc d’or écrit avant l’arrêt, les colonnes
|
||||
de blocs, biomes, densités et le tonneau. Le codec sauvegardé a aussi été décodé.
|
||||
Le framework GameTest reconstruit ses réglages depuis le preset : ce contrôle
|
||||
ne remplace pas un essai du parcours normal de chargement dans le client.
|
||||
La reprise d’un journal réservé est contrôlée séparément par le smoke ; un
|
||||
crash forcé en pleine génération, les téléportations de vrais joueurs et la
|
||||
charge multijoueur ne sont pas simulés.
|
||||
|
||||
Les smokes parcourent cinq graines, les quatre tailles et les quatre reliefs :
|
||||
transformation verticale monotone, dessous de l’origine conservé, canyon retirant
|
||||
de la matière et couronne volcanique. Ce sont des contrôles géométriques ; les
|
||||
profils 128/256/512 et les quatre reliefs demandent encore une appréciation
|
||||
visuelle en jeu. Voir [Validation](testing.md) pour les commandes et preuves.
|
||||
@@ -1,212 +0,0 @@
|
||||
# Alpha.13 — un monde Sanctuary unifié
|
||||
|
||||
**Publiée et synchronisée dans la même instance Prism.** Cette version réunit l’île de départ complète et les outils
|
||||
d’expansion du laboratoire.
|
||||
[Validation](testing.md) décrit les preuves disponibles et
|
||||
[Distribution](packwiz.md) indique la version effectivement publiée.
|
||||
|
||||
La cible reste **Minecraft 26.3-pre-2 / Fabric**. Ce guide concerne un nouveau
|
||||
monde alpha.13. Les anciens mondes conservent leur génération.
|
||||
|
||||
## Créer son monde
|
||||
|
||||
1. Créer un **nouveau monde**, puis sélectionner le type **Sanctuary**.
|
||||
2. Ouvrir le bouton natif **Personnaliser**.
|
||||
3. Choisir la taille, puis cliquer sur **Terminé** avant de créer le monde.
|
||||
|
||||
| Taille | Diamètre nominal de l’île initiale |
|
||||
| --- | --- |
|
||||
| Petit | 512 blocs |
|
||||
| Moyen, choix par défaut | 724 blocs |
|
||||
| Grand | 1 024 blocs |
|
||||
|
||||
**Annuler** revient sans appliquer le changement. La hauteur reste de 384 blocs,
|
||||
de `Y=0` à `Y=383`. Le diamètre est une référence pour la forme : trous, surplombs,
|
||||
fragments et contour irrégulier dépendent de la graine. Il ne mesure pas une
|
||||
surface entièrement constructible et ne fixe pas un nombre de joueurs.
|
||||
|
||||
La liste des types de monde contient un seul choix Sanctuary ; les types
|
||||
Minecraft habituels restent disponibles. Petit, Moyen et Grand sont des
|
||||
réglages de ce même choix. L’ancien Grand alpha.11, de 724 blocs, correspond
|
||||
au diamètre nominal du nouveau Moyen.
|
||||
|
||||
Sur un serveur neuf, le preset suivant crée un monde de taille Moyen :
|
||||
|
||||
```properties
|
||||
level-type=sanctuary:sanctuary
|
||||
level-name=sanctuary-dev
|
||||
level-seed=42
|
||||
```
|
||||
|
||||
Choisir un dossier de monde neuf pour un essai. Le bouton **Personnaliser**
|
||||
configure la création en solo ; il ne modifie pas les fichiers d’un serveur
|
||||
existant. Installer le mod ou changer `level-type` ne transforme pas une partie.
|
||||
|
||||
## L’île de départ
|
||||
|
||||
Le nouveau monde reprend le générateur complet de l’île initiale : forêts de
|
||||
chênes et de bouleaux, clairières, affleurements, strates rocheuses et corniches
|
||||
intérieures. Son relief flottant conserve les trous et les surplombs, sans
|
||||
ajouter de noyau cylindrique pour garantir un terrain à l’origine.
|
||||
|
||||
Les plans hydrologiques de Population sont repris : étangs et lacs, recherche
|
||||
de rivière, bassins étagés et leurs débordements, sources et cascades, poche et
|
||||
coulées de lave. Le placement dépend des supports réellement présents dans la
|
||||
roche. Un plan qui ne trouve pas de site admissible reste absent ; aucune
|
||||
rivière, terrasse ou ressource n’est garantie sur chaque graine. Les eaux en
|
||||
terrasses permettent la nage verticale sans ajouter de culture de riz.
|
||||
|
||||
Les poches souterraines natives du laboratoire rejoignent cette île : cavités
|
||||
luxuriantes, à spéléothèmes ou soufrées, avec leurs décorations Minecraft.
|
||||
Il s’agit toujours des profondeurs de l’Overworld, pas de la dimension des
|
||||
cavernes prévue dans la vision. L’île garde sa palette tempérée en surface ;
|
||||
Dappled Forest reste disponible dans les expansions au climat automnal.
|
||||
|
||||
L’intégration protège les volumes déclarés par les plans d’eau et de lave
|
||||
contre les écritures de décoration suivantes. Cette protection concerne les
|
||||
plans de génération, sans mettre les blocs à l’abri des actions des joueurs.
|
||||
Les preuves moteur et leurs limites sont suivies dans [Validation](testing.md).
|
||||
|
||||
## Ajouter une île
|
||||
|
||||
Les commandes demandent les commandes autorisées en solo ou la permission
|
||||
opérateur sur le serveur. Elles reprennent les outils alpha.12.1 dans le nouveau
|
||||
monde Sanctuary. L’économie, les recherches et les contributions collectives
|
||||
ne conditionnent pas encore leur utilisation.
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick north autumn 128 peaks
|
||||
/sanctuary expansion list
|
||||
/sanctuary expansion visit island_1
|
||||
/sanctuary expansion create oasis island_1 east arid 64 natural 2048
|
||||
```
|
||||
|
||||
`quick` ouvre une île depuis Sanctuary et lui attribue un identifiant `island_N`.
|
||||
L’exemple de visite suppose qu’il s’agit de la première réservation ; `list`
|
||||
indique l’identifiant et l’état réels. La visite attend que l’île soit prête.
|
||||
`create` choisit un nom et un parent déjà prêt pour prolonger une branche.
|
||||
La création ne téléporte personne implicitement.
|
||||
|
||||
Les nouveaux continents choisissent leur propre direction, climat, diamètre
|
||||
**64/128/256/512** et relief **natural/peaks/canyon/volcano**. La direction ne
|
||||
contraint pas le climat. L’autocomplétion fournit les huit directions et les
|
||||
douze profils : `temperate`, `boreal`, `glacial`, `cold_humid`, `humid`, `tropical`,
|
||||
`warm`, `arid`, `dry`, `cold_dry`, `autumn`, `cherry`.
|
||||
|
||||
Le dernier nombre facultatif est une **distance minimale entre les centres**
|
||||
du parent et de l’enfant, entre 0 et 100 000 blocs. Une valeur de 0 ou son
|
||||
omission laisse le placement automatique. Les marges et l’alignement sur les
|
||||
chunks peuvent éloigner le centre retenu.
|
||||
|
||||
Si le premier site est occupé, la recherche continue plus loin dans la même
|
||||
direction : au plus 32 positions et 30 secondes de lecture au total. Même du
|
||||
vide déjà généré compte comme occupé. Une erreur disque arrête la demande ;
|
||||
elle n’autorise pas à remplacer les chunks concernés.
|
||||
|
||||
```text
|
||||
/sanctuary expansion preview glacier sanctuary northeast glacial 256 canyon 2048
|
||||
/sanctuary expansion resume island_1
|
||||
```
|
||||
|
||||
`preview` calcule une proposition géométrique sans activer l’île et sans vérifier
|
||||
l’occupation ou les fondations. `resume` reprend une réservation interrompue,
|
||||
avec sa graine et ses paramètres conservés. Le système ne fournit aucune commande
|
||||
de régénération des régions déjà créées.
|
||||
|
||||
Les îles ajoutées gardent la génération d’expansion alpha.12 : leurs climats,
|
||||
reliefs, décorations et structures compatibles. Elles ne reçoivent pas
|
||||
systématiquement la grande rivière ou les terrasses de l’île initiale. Villages,
|
||||
temples, cabanes de marais et camps abandonnés dépendent du biome, du diamètre
|
||||
et des fondations ; aucune présence n’est garantie. Les règles de progression
|
||||
futures restent décrites dans [Expansion](expansion.md).
|
||||
|
||||
## Correctif alpha.13.1 — expansions sous Windows
|
||||
|
||||
L’erreur `AccessDeniedException` visant le **dossier**
|
||||
`data/sanctuary-world-v13`, depuis `ExpansionJournal.persist`, vient de la
|
||||
synchronisation du répertoire après l’écriture du journal. Java sous Windows
|
||||
ne permet pas cette ouverture avec `FileChannel` ; ce message ne signifie pas
|
||||
à lui seul que le joueur manque de droits sur sa sauvegarde.
|
||||
|
||||
1. Fermer Minecraft pour arrêter la session dont les expansions sont bloquées.
|
||||
2. Mettre l’instance Prism existante à jour vers **0.1.0-alpha.13.1** via son
|
||||
canal packwiz, puis relancer le jeu.
|
||||
3. Rouvrir **le même monde** et consulter `/sanctuary expansion list`.
|
||||
|
||||
Si l’instance Windows a été importée depuis le MRpack sans auto-update, fermer
|
||||
Minecraft puis remplacer seulement `sanctuary-0.1.0-alpha.13.jar` par le JAR
|
||||
`sanctuary-0.1.0-alpha.13.1.jar` de la release dans son dossier `mods`.
|
||||
Conserver un seul JAR Sanctuary actif ; les autres mods et les sauvegardes
|
||||
restent dans la même instance. Le raccordement au canal stable est décrit dans
|
||||
[Distribution](packwiz.md#raccorder-linstance-prism-existante).
|
||||
|
||||
Le remplacement du journal ayant précédé cette erreur, l’expansion demandée
|
||||
peut déjà être enregistrée comme `reserved`. Au chargement, les réservations
|
||||
valides reprennent automatiquement avec leur identifiant, leur graine et leur
|
||||
position. Il n’est pas nécessaire de recréer le monde ou de supprimer le dossier
|
||||
du journal. La commande `resume` seule ne débloque pas une session dont le
|
||||
journal a été déclaré indisponible : sa relecture exige de rouvrir le monde.
|
||||
|
||||
Le correctif conserve le format et le contenu du journal existant ; il ne
|
||||
migre ni ne régénère les chunks. La [validation ciblée](testing.md#correctif-alpha131--journal-dexpansion-sous-windows)
|
||||
distingue les contrôles locaux de l’essai Windows natif restant à effectuer.
|
||||
|
||||
## Paramètres et sauvegardes
|
||||
|
||||
Le preset public `sanctuary:sanctuary` utilise le nouveau codec de générateur
|
||||
`sanctuary:island_v13`. Sa source de biomes est également enregistrée sous
|
||||
`sanctuary:island_v13`. Le générateur sérialise `initial_diameter`, limité à
|
||||
512, 724 ou 1 024 ; une valeur absente prend 724. Ce champ appartient à la
|
||||
sauvegarde créée, et ne suit pas les changements ultérieurs de défaut du pack.
|
||||
|
||||
Pour l’île initiale, les réglages internes `sanctuary:unified_5`,
|
||||
`sanctuary:unified_10` et `sanctuary:unified_20` correspondent respectivement
|
||||
aux diamètres 512, 724 et 1 024. Les suffixes sont des références de calcul,
|
||||
pas des capacités affichées. Ils reprennent la densité Population et son
|
||||
hydrologie avec le générateur et le `RandomState` propres à l’origine.
|
||||
|
||||
La source combine la palette Population de départ et le sélecteur de cavités
|
||||
natives du laboratoire, avec les mêmes conditions de sélection de ces poches.
|
||||
Les îles enfants conservent le sélecteur climatique du laboratoire. Les 34
|
||||
biomes natifs de cette palette et les dix biomes Population sont déclarés
|
||||
avant la première expansion ; TerraMix n’est pas une dépendance téléchargée.
|
||||
|
||||
Le nouveau journal est enregistré dans le dossier du monde :
|
||||
|
||||
```text
|
||||
data/sanctuary-world-v13/expansions.json
|
||||
```
|
||||
|
||||
Son schéma reste **1**, avec une racine de **génération 13** et des extensions
|
||||
de **génération 12**. Les identifiants, parents, graines, paramètres, positions
|
||||
et états d’ouverture sont conservés côté serveur. La racine et le diamètre
|
||||
initial ne sont pas modifiables par une commande d’expansion.
|
||||
|
||||
Les mondes du laboratoire alpha.12/12.1 gardent leur codec et leur journal
|
||||
`data/sanctuary-expansion-v1/expansions.json`. Les générations Population et
|
||||
les autres générations antérieures gardent leurs paramètres enregistrés.
|
||||
Les presets historiques sont conservés mais cachés dans la création publique.
|
||||
**Aucun monde n’est converti, agrandi, réduit ou régénéré par l’alpha.13.**
|
||||
Le [guide alpha.12](alpha12-laboratory.md) est une référence historique, pas une
|
||||
instruction de sélectionner à nouveau le laboratoire pour cette version.
|
||||
|
||||
## Contrôles de développement
|
||||
|
||||
`./gradlew check build assemblePack` et `:sanctuary:runGameTest` sélectionnent
|
||||
par défaut la suite unifiée. Son diamètre de test se choisit avec
|
||||
`-PsanctuaryTestDiameter=512`, `724` ou `1024` ; il ne change pas le défaut public.
|
||||
|
||||
Les suites Population historiques demandent `-PsanctuaryUnifiedTests=false`,
|
||||
avec leur profil `-PsanctuaryTestPlayers=5`, `10`, `20` ou `100`. Les essais du
|
||||
laboratoire historique utilisent aussi ce drapeau, avec
|
||||
`-PsanctuaryExpansionTests=true`. Ces processus travaillent dans les dossiers
|
||||
de développement ignorés, jamais dans les sauvegardes personnelles.
|
||||
|
||||
Les validations et mesures des alpha.11, .12 et .12.1 restent historiques.
|
||||
Elles ne prouvent ni les temps de chargement ni le rendu de cette intégration.
|
||||
Les trois tailles, l’interface native et les expansions/rechargements passent
|
||||
les contrôles alpha.13. Moyen atteint 63,66 s pour créer le monde puis charger
|
||||
289 chunks FULL sur la graine 0 ; voir [Validation](testing.md) pour le protocole
|
||||
et ses limites. La release, le MRpack et le canal packwiz sont publiés ; deux
|
||||
synchronisations dans la même instance Prism conservent les 601 fichiers
|
||||
personnels et réglages suivis. Voir [Distribution](packwiz.md).
|
||||
@@ -1,115 +0,0 @@
|
||||
# Architecture de l’alpha.20
|
||||
|
||||
Cette passe donne une identité visuelle aux lieux déjà planifiés : volumes,
|
||||
façades, sols, plafonds et mobilier. Elle réutilise les emprises et les accès
|
||||
certifiés des générations précédentes. Les bâtiments de ville et les sites
|
||||
expérimentaux restent inscrits dans leurs parcelles de 17 × 17 blocs ; leurs
|
||||
corniches et leurs retraits restent à l’intérieur de cette limite.
|
||||
|
||||
Les nouveaux rendus demandent un **nouveau monde de génération 20**. Les mondes
|
||||
19 et antérieurs conservent leurs bâtiments, y compris dans les futurs chunks.
|
||||
Le bouton **Personnaliser**, les trois tailles et l’option **Structures
|
||||
expérimentales** restent disponibles. Le catalogue n’impose pas que chaque lieu
|
||||
apparaisse sur chaque île.
|
||||
|
||||

|
||||
|
||||
*Vues des blocs réellement émis par les générateurs, affichés avec les modèles
|
||||
Minecraft, sans terrain. Ce montage est une prévisualisation géométrique, pas
|
||||
une capture du client en jeu ; il montre une sélection du catalogue.*
|
||||
|
||||
## Les six bâtiments de Lost City
|
||||
|
||||
Les étages conservent leurs plans alternés, leurs pièces et leurs escaliers.
|
||||
Les sanitaires sont clos, les lits adossés à de vrais murs, et les lampes
|
||||
reposent sur des blocs complets. Les aménagements de toit décrits ci-dessous
|
||||
possèdent des points d’accès vérifiés par le smoke du renderer.
|
||||
|
||||
| Fonction | Forme et façade | Espaces et détails construits |
|
||||
| --- | --- | --- |
|
||||
| Immeuble | Angles avant coupés, baies en saillie, montants de chêne écorcé et bandeaux de tuf. | Parquets alternant chêne et bouleau, logements avec cuisine et chambre ; jardinières, portique de bois et table sur le toit. |
|
||||
| Hôtel | Portique et travées encadrées de diorite polie, garde-corps et bandeaux en bouleau ; pavillon de toit en retrait. | Réception, chambres, salons, sanitaires et buanderie ; petit local de linge et bancs sur le toit. |
|
||||
| Bibliothèque | Grandes baies, angles coupés et lames verticales en bouleau écorcé ; lanterneau vitré avec ouverture à travers le toit et le plafond du dernier étage. | Rayonnages, archives, tables et lutrins ; sols délimitant les espaces de lecture et terrasse de lecture sur le toit. |
|
||||
| Restaurant | Encadrements de granit poli, vitrines et auvents associant chêne et bouleau. | Salles à manger et cuisines réparties par étage, garde-manger et sanitaires ; tables sur le toit et sorties d’extraction maçonnées. |
|
||||
| Caserne | Piles épaisses de briques de tuf, soubassements ciselés et fenêtres grillagées. | Dortoirs, bureau de service, réserves d’équipement et sanitaires ; cage d’escalier coiffée d’un pavillon accessible. |
|
||||
| Hall | Travées monumentales, piliers ciselés, corniches de diorite et fronton de toit en gradins. | Billetterie, bureaux, salle avec scène sur deux niveaux, galerie protégée, salles de réunion et archives. |
|
||||
|
||||
## Les quatorze anciennes infrastructures
|
||||
|
||||
Chaque site conserve ses seuils, son accès de service et son coffre de découverte.
|
||||
Les fonctions ci-dessous sont exprimées par les pièces et les blocs vanilla
|
||||
présents ; elles ne constituent pas de nouveaux systèmes de progression.
|
||||
|
||||
| Lieu | Architecture et usage visibles |
|
||||
| --- | --- |
|
||||
| Gare de fret | Halle aux angles coupés, lanterneau vitré et long auvent de quai en chêne ; voie, salle d’attente et petit bureau de signalisation. |
|
||||
| Poste et centre de tri | Façade de granit, accents de diorite et toiture en retrait ; guichet, rangées de tonneaux, passage de tri et bureau. |
|
||||
| Dépôt communal | Grand entrepôt nervuré de pierre, toiture industrielle à trois travées vitrées en dents de scie ; rails, chargement, caisses et mezzanine d’inventaire. |
|
||||
| Cartothèque d’expédition | Corps en bouleau, petites ailes différenciées et balcon arrière abrité ; table de cartographie, lutrin, archives et réserve. |
|
||||
| Observatoire et météo | Tour à pans coupés, contreforts de diorite et couronne ouverte portée par des nervures ; deux volées d’escalier, salle d’instruments et plateforme d’observation. |
|
||||
| Atelier ferroviaire | Large couverture à nervures et vitrages, piles de granit ; deux voies, fosse d’entretien avec escalier, établi, fourneau, meule et petite réserve. |
|
||||
| Station de pompage et château d’eau | Bâtiment bas, conduites figurées par des grilles et murets, cuve haute sur quatre piles ciselées ; galerie d’entretien et réserve sèche. L’eau reste dans la cuve. |
|
||||
| Sous-station | Cour clôturée, supports et grilles autour des équipements, locaux bas à toiture nervurée ; commandes vanilla et tranchée d’entretien accessible. |
|
||||
| Serre et graineterie | Verrière haute rythmée par des nervures de bouleau ; cultures, deux points d’irrigation contenus, allée de tuf et remise de rempotage. |
|
||||
| Quai sur le vide | Platelage en chêne, garde-corps à piliers sculptés, abris bas et grue de bois à chaîne ; attente, débarcadère et réserve. |
|
||||
| Relais radio et maison du gardien | Petite maison de chêne avec toiture à deux matières et cheminée ; antenne haute à traverses, plateforme accessible et chambre du gardien. |
|
||||
| Club photo et chambre noire | Façade claire de diorite, corniche sculptée et toiture arrière en retrait ; salle du club, chambre noire, bacs et espace de séchage à chaînes. |
|
||||
| Vestiaires et salles étranges | Entrée basse et étroite suivie d’une salle haute à colonnes répétées ; casiers, sols carrelés, alcôve et petite pièce de service. |
|
||||
| Refuge de chantier | Cabane surélevée sur piles ciselées, toiture inclinée et ossature inachevée ; couchage, établi, escalier extérieur et trappe avec échelle vers la réserve inférieure. |
|
||||
|
||||
Les voies de fret gardent leur ballast, leurs traverses, leurs courbes et leurs
|
||||
rails manquants. Les chemins reçoivent des bordures de briques de tuf et des
|
||||
accents ciselés ; leurs pentes et leurs destinations restent celles du plan
|
||||
sauvegardé.
|
||||
|
||||
## Espaces publics et équipements extérieurs
|
||||
|
||||
| Espace | Détails présents en génération 20 |
|
||||
| --- | --- |
|
||||
| Rues et ponts | Chaussée d’andésite polie, bordures de tuf poli, bandes de briques de tuf ponctuées de blocs ciselés ; marches sur les pentes, piles de garde-corps et grilles sur les ponts. Les tabliers n’ajoutent pas de colonnes vers le vide. |
|
||||
| Parc arboré | Allées en croix, bancs de chêne à accoudoirs minéraux, lampadaires et petite couronne de bouleau à côté des arbres existants. |
|
||||
| Jardin | Quatre parterres bordés de diorite, angles de tuf sculpté, portique de bois et promenade centrale. |
|
||||
| Aire de jeux | Bac à sable bordé de bouleau, portique de balançoire, plateforme de jeu avec marches et garde-corps, bancs et éclairage. |
|
||||
| Place | Sol organisé en couronnes de diorite, granit et tuf ; fontaine sèche à coupe basse et piédestal sculpté. |
|
||||
| Mémorial | Sol minéral à lignes de tuf, marches autour du socle, colonne mêlant tuf et briques de pierre sculptées. |
|
||||
| Marché | Croisement piéton, quatre étals avec pieds ciselés, poteaux de chêne et couvertures alternant dalles de chêne et de bouleau. |
|
||||
| Parking | Rampe et stationnements conservés ; piliers d’andésite, assises ciselées, bandeau en tuf, grilles de ventilation et lampes de plafond. Il reste enterré seulement lorsque le sol est certifié. |
|
||||
| Piscine extérieure | Bassin et marches immergées conservés, margelles de diorite, chaises longues et auvent de bouleau près des bacs de douche. |
|
||||
| Excavation | Escalier central vers la fosse, barrières, piles sculptées, poutre sur appuis, établi et meule ; le chantier reste limité à la roche certifiée. |
|
||||
|
||||
## Sous la ville et réseaux oubliés
|
||||
|
||||
| Ensemble | Traitement architectural |
|
||||
| --- | --- |
|
||||
| Grande piscine couverte | Bassin, escaliers et colonnade conservés ; parois de calcite, bandeaux de diorite, chapiteaux ciselés et nervures hautes répétées. |
|
||||
| Parkings souterrains | Plateformes et rampes conservées ; fondations maçonnées et bandeaux de tuf ponctués de motifs ciselés. |
|
||||
| Grande salle sèche | Boiseries de chêne, assises de tuf, murs clairs, corniches et bancs de chêne ; le socle central reste sans machine de progression. |
|
||||
| Égouts et sorties d’eau | Briques de tuf, motifs ciselés et corniches hautes ; les canaux, passerelles et écoulements existants sont conservés. |
|
||||
| Passages et accès | Liserés de granit ou de tuf, seuils et accès verticaux conservés. |
|
||||
| Mines, ateliers et réserves oubliés | Poutres de chêne, tuf, sols bordés de granit, établi et outillage vanilla ; rails, réserves et plateforme de travaux interrompus. |
|
||||
| Archives et salles étranges | Parquets de bouleau, boiseries, rayonnages ; ailleurs, carrelages de calcite et diorite, colonnes et niches répétées. |
|
||||
| Salles techniques oubliées | Nervures minérales, corniches et encadrements ciselés autour des seuils ; pompes figurées, grilles et postes de commande vanilla. |
|
||||
|
||||
## Matériaux et raccords
|
||||
|
||||
Les fondations des nouveaux bâtiments et chemins sont en **briques de pierre**.
|
||||
La stone et la smooth stone restent présentes ; l’andésite, la diorite et le
|
||||
granit différencient les sols et les usages. Le tuf existe brut, poli, en
|
||||
briques et sous ses deux formes ciselées. Les dalles, escaliers inversés,
|
||||
murets, grilles et trappes donnent de l’épaisseur aux corniches, aux appuis de
|
||||
fenêtre et aux équipements. Le chêne et le bouleau apparaissent comme poteaux,
|
||||
boiseries, parquets, charpentes et couvertures selon les lieux.
|
||||
|
||||
Le rendu 20 calcule les raccords des vitres, barreaux, clôtures, murets et
|
||||
angles d’escaliers à partir de tous les blocs de la pièce, après sa rotation
|
||||
et avant son découpage entre chunks. Une fenêtre peut ainsi rejoindre son
|
||||
véritable encadrement, même lorsque le chargement découpe le bâtiment.
|
||||
|
||||
Les smokes des bâtiments de ville et des espaces extérieurs passent, notamment
|
||||
pour les accès, les sanitaires, les lits, les lampes, les rampes et les bassins.
|
||||
Les essais natifs de création et de réouverture de la ville et des infrastructures
|
||||
passent ; leurs résultats et les contrôles de compatibilité sont suivis dans [les essais alpha.20](testing-alpha20.md).
|
||||
|
||||
Aucune caméra, mailbox, machine d’expansion ou production électrique dédiée
|
||||
n’est ajoutée par cette passe. Les meubles utilisables restent les blocs
|
||||
vanilla, et les butins de découverte restent partagés et non renouvelés.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Alpha.21 — palettes et couvertures
|
||||
|
||||
La ville revient à une base commune : pierre lisse, pierre, briques de pierre,
|
||||
andésite et chêne. Les façades, seuils, corniches et verrières gardent leurs
|
||||
formes ; les pièces conservent leurs fonctions et leur circulation. La serre
|
||||
emploie le bouleau comme bois propre. Le tuf accompagne les routes et les
|
||||
parties souterraines.
|
||||
|
||||

|
||||
|
||||
Ces vues sont calculées depuis les blocs produits par les renderers21 et les
|
||||
modèles/textures de Minecraft 26.3-pre-2. Elles montrent des variantes choisies,
|
||||
sans terrain, éclairage ni végétation de la partie. Ce ne sont pas des captures
|
||||
du client et elles ne prouvent pas les raccords avec le relief.
|
||||
|
||||
Les couvertures de l’atelier, du dépôt, de la serre, du relais radio et du refuge
|
||||
reposent sur des murs ou charpentes. L’ancien plafond opaque sous leur nouvelle
|
||||
couverture est retiré. Les vérifications contrôlent aussi les petits appuis des
|
||||
excavations, piscines, rampes de garage et ornements.
|
||||
|
||||

|
||||
|
||||
La seconde planche coupe volontairement les murs pour voir les dessous des toits.
|
||||
Les détails de validation et la graine d’essai sont dans [le protocole](testing-alpha21.md).
|
||||
-1039
File diff suppressed because it is too large
Load Diff
@@ -1,654 +0,0 @@
|
||||
{
|
||||
"ticket": "WG-12",
|
||||
"version": "0.1.0-alpha.11",
|
||||
"date": "2026-09-09",
|
||||
"protocol": {
|
||||
"machine": "Apple M1, 8 GiB RAM, macOS",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"fabric_loader": "0.19.5",
|
||||
"fabric_api": "0.160.0+26.3",
|
||||
"baseline_source_commit": "a26b72f",
|
||||
"production_files_changed_in_baseline": 0,
|
||||
"runs_per_configuration": 1,
|
||||
"world": "fresh ignored development world per run",
|
||||
"workload": "289 synchronous FULL chunks in concentric rings around natural spawn, then 80 FULL chunks in an eastward corridor. Minecraft world creation timed separately from startup log.",
|
||||
"scope": "Server generation up to FULL. Does not establish ticking readiness, postprocessing completion, client rendering, FPS, multiplayer capacity or a guaranteed user loading time.",
|
||||
"baseline_scope_correction": "The raw alpha.10 report incorrectly says postprocessing is included; FULL alone does not guarantee this. Workload code is identical; alpha.11 corrects only that explanatory string.",
|
||||
"build_time_excluded": true,
|
||||
"comparison": "Only legacy20 versus legacy20 measures the algorithm gain. Grand10 changes geometry; its exploration timing cannot be used for that comparison.",
|
||||
"frozen_runtime_files": 272,
|
||||
"frozen_runtime_manifest_sha256": "1c2b433417a5d9e6a219e9ee19781574538d2167c774e14ed2bf254d277d4482",
|
||||
"baseline_runtime_manifest_sha256": "d4860072d1896f0dfd784a5876f1253367b7818a2023f1910ba1612b6f0e7103",
|
||||
"benchmark_source_sha256": "408711c97d65de63af4e0a1eac90e6edb5bf2df2b70c43772a3fdef7347d75d1"
|
||||
},
|
||||
"runs": {
|
||||
"alpha10_legacy20": {
|
||||
"version": "0.1.0-alpha.10",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 310664.163542,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"elapsed_ms": 2.609,
|
||||
"cumulative_chunks": 1,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5468.709333,
|
||||
"cumulative_chunks": 9,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 17601.560917,
|
||||
"cumulative_chunks": 25,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 27397.566458,
|
||||
"cumulative_chunks": 49,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 36562.853333,
|
||||
"cumulative_chunks": 81,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 44462.775334,
|
||||
"cumulative_chunks": 121,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 52423.80275,
|
||||
"cumulative_chunks": 169,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 60416.705916,
|
||||
"cumulative_chunks": 225,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 66303.128125,
|
||||
"cumulative_chunks": 289,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"median": 993.3335,
|
||||
"p95": 1392.158958,
|
||||
"max": 2560.16575
|
||||
},
|
||||
"exploration_ms": 103141.834334,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 5146.88675,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5087.495583,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5229.782834,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5046.559,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4810.5415,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 28755.95025,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4763.415834,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4772.209166,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4826.460208,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4857.054416,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4947.188167,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5168.714916,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4897.048167,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5286.23025,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4865.694458,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4661.793875,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"median": 925.461875,
|
||||
"p95": 1273.829625,
|
||||
"max": 25131.068417
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 96766,
|
||||
"world_creation_plus_spawn_area_ms": 407430.163542,
|
||||
"raw_report_sha256": "d70c4ac49f37470d4db15b9200e26da8ac9d4d3b1b008e7ec03e67deafcc0f93",
|
||||
"log_sha256": "5b1df55f7a167003c089e80fe1fb824d087c137e3662bac84af3ae0b325b8392"
|
||||
},
|
||||
"alpha11_legacy20": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 43647.054375,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 1.965667,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 870.4725,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 2972.902334,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4480.355667,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5120.230375,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6290.425208,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7381.384417,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7839.425333,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8677.269209,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"p95": 280.6,
|
||||
"median": 135.289625,
|
||||
"max": 699.612708
|
||||
},
|
||||
"exploration_ms": 14051.250167,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"chunk_x": 9,
|
||||
"elapsed_ms": 750.152875
|
||||
},
|
||||
{
|
||||
"chunk_x": 10,
|
||||
"elapsed_ms": 680.650584
|
||||
},
|
||||
{
|
||||
"chunk_x": 11,
|
||||
"elapsed_ms": 729.180958
|
||||
},
|
||||
{
|
||||
"chunk_x": 12,
|
||||
"elapsed_ms": 725.223
|
||||
},
|
||||
{
|
||||
"chunk_x": 13,
|
||||
"elapsed_ms": 540.200125
|
||||
},
|
||||
{
|
||||
"chunk_x": 14,
|
||||
"elapsed_ms": 5554.177667
|
||||
},
|
||||
{
|
||||
"chunk_x": 15,
|
||||
"elapsed_ms": 473.982208
|
||||
},
|
||||
{
|
||||
"chunk_x": 16,
|
||||
"elapsed_ms": 471.05575
|
||||
},
|
||||
{
|
||||
"chunk_x": 17,
|
||||
"elapsed_ms": 513.109667
|
||||
},
|
||||
{
|
||||
"chunk_x": 18,
|
||||
"elapsed_ms": 533.284292
|
||||
},
|
||||
{
|
||||
"chunk_x": 19,
|
||||
"elapsed_ms": 503.983917
|
||||
},
|
||||
{
|
||||
"chunk_x": 20,
|
||||
"elapsed_ms": 513.126292
|
||||
},
|
||||
{
|
||||
"chunk_x": 21,
|
||||
"elapsed_ms": 508.7555
|
||||
},
|
||||
{
|
||||
"chunk_x": 22,
|
||||
"elapsed_ms": 587.331416
|
||||
},
|
||||
{
|
||||
"chunk_x": 23,
|
||||
"elapsed_ms": 467.628834
|
||||
},
|
||||
{
|
||||
"chunk_x": 24,
|
||||
"elapsed_ms": 494.771375
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"p95": 186.482333,
|
||||
"median": 103.648708,
|
||||
"max": 5153.6295
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18888,
|
||||
"world_creation_plus_spawn_area_ms": 62535.054375,
|
||||
"raw_report_sha256": "4799180bd61ec4ffc93d6a1a7ccb5440eb880f7dc08a345429b77db10cc17ab0",
|
||||
"log_sha256": "a9347297608528e719d6780f3dd2f2588bb516e06ab771ca6da6ffdda404f7a4"
|
||||
},
|
||||
"alpha11_grand10": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 10,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 44291.159875,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"radius": 0,
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 42.387459
|
||||
},
|
||||
{
|
||||
"radius": 1,
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 1474.290584
|
||||
},
|
||||
{
|
||||
"radius": 2,
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 3180.279791
|
||||
},
|
||||
{
|
||||
"radius": 3,
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4474.062208
|
||||
},
|
||||
{
|
||||
"radius": 4,
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5043.704042
|
||||
},
|
||||
{
|
||||
"radius": 5,
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6367.417708
|
||||
},
|
||||
{
|
||||
"radius": 6,
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7285.252542
|
||||
},
|
||||
{
|
||||
"radius": 7,
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7824.965875
|
||||
},
|
||||
{
|
||||
"radius": 8,
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8577.641208
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"max": 665.612875,
|
||||
"p95": 274.9215,
|
||||
"median": 136.067375
|
||||
},
|
||||
"exploration_ms": 7684.81575,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 739.863,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 724.49625,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 720.754584,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 695.608958,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 522.1695,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 1112.798042,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 430.820667,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 416.082834,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 400.90425,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 414.387708,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 394.878458,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 393.643834,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 391.553625,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 279.306334,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 25.634541,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 18.210167,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"max": 760.323917,
|
||||
"p95": 198.353916,
|
||||
"median": 85.88425
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18376,
|
||||
"world_creation_plus_spawn_area_ms": 62667.159875,
|
||||
"raw_report_sha256": "5284e0ef88217f01d51f9040bf13f652de8276dc3f9844dc14569848b227d05a",
|
||||
"log_sha256": "3a4c84bb6ea1614f8fc9c85f472612750149996e2f7658c83afe398b43afe5f6"
|
||||
}
|
||||
},
|
||||
"same_geometry_reduction_percent": {
|
||||
"world_creation_plus_spawn_area_ms": 84.65134396742975,
|
||||
"spawn_area_ms": 85.95040577665497,
|
||||
"exploration_ms": 86.37676917641545
|
||||
},
|
||||
"targets": {
|
||||
"minimum_reduction_percent": 30,
|
||||
"grand_creation_plus_spawn_area_max_ms": 120000,
|
||||
"same_geometry_reduction_passed": true,
|
||||
"grand_generation_proxy_passed": true
|
||||
},
|
||||
"density_equivalence": {
|
||||
"comparison": "Float.floatToRawIntBits; no tolerance",
|
||||
"reference_commit": "a26b72f",
|
||||
"scope": "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes",
|
||||
"reference_source_sha256": "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"profiles": [
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "28ec59cba36174fd"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "efd1e8084800ed5c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "91bea1db755d9aca"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "b7bab667baf73361"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "42aeaf160360e41c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "cb85f95333e040c0"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "175524360755dd88"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "4cc8aec467a2d164"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "5a3f16e426e5a64e"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "83cda17336772fa7"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "e7aa7f52022cc6aa"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "de4b8f52d249a9bd"
|
||||
}
|
||||
],
|
||||
"concurrent_workers": 3,
|
||||
"concurrent_comparisons": 115668,
|
||||
"passed": true,
|
||||
"adversarial": {
|
||||
"aligned_scalar_path_preserved": true,
|
||||
"passed": true,
|
||||
"bitwise_comparisons": 122528,
|
||||
"concurrent_workers": 3,
|
||||
"repeated_cell_corner_batches": 1
|
||||
}
|
||||
},
|
||||
"persistence": {
|
||||
"default_players": 10,
|
||||
"profiles": [
|
||||
{
|
||||
"restored_settings": "sanctuary:population_5",
|
||||
"players": 5,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_5"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_10",
|
||||
"players": 10,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_20",
|
||||
"players": 20,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_20"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_100",
|
||||
"players": 100,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_100"
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"visible_presets": [
|
||||
"sanctuary:sanctuary_5",
|
||||
"sanctuary:sanctuary"
|
||||
],
|
||||
"seed": 0,
|
||||
"legacy_settings_preserved": "sanctuary:sanctuary_cavern",
|
||||
"omitted_density_capacity_preserved": 20
|
||||
},
|
||||
"limits": [
|
||||
"Single seed and one timed run per configuration; repeat to estimate variance.",
|
||||
"A new cold region still has a synchronous planning cost; observed legacy20 maximum exploration chunk latency is about 5.15 s.",
|
||||
"No personal saved world was opened or regenerated."
|
||||
],
|
||||
"engine_checks": {
|
||||
"5": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_5",
|
||||
"inspected_full_chunks": 110,
|
||||
"retained_water_blocks": 5656,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "cfaa0a532b50399ab28808f51a871363b198a67d8c096ac13f7a011bd87ff9d0"
|
||||
},
|
||||
"10": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_10",
|
||||
"inspected_full_chunks": 125,
|
||||
"retained_water_blocks": 5923,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "aaed5b38821197c479a8acb7f2a201c64f290666c039d0b15047340e403e028c"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.12",
|
||||
"date": "2026-09-09",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"java": 25,
|
||||
"generation": 12,
|
||||
"source_base_commit": "f816ffc6402fe31789f9677c2ee94328b3181329",
|
||||
"scope": "Local development tests only; no personal worlds, client rendering, live player teleport or multiplayer load.",
|
||||
"normalization": "world_directory fields replaced with <development_game_test_world>; all other diagnostic fields retained.",
|
||||
"artifact": {
|
||||
"path": "mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.12.jar",
|
||||
"sha256": "f2ac2462a03a39ef330cc52143f9753ac5908b606cfe1dd1388ad5caae15e6e9",
|
||||
"staged_pack_jar_identical": true,
|
||||
"contains_gametest_classes": false
|
||||
},
|
||||
"build": {
|
||||
"command": "./gradlew check build assemblePack",
|
||||
"passed": true,
|
||||
"legacy_profile": 10,
|
||||
"legacy_seed": 0,
|
||||
"required_engine_tests": 12,
|
||||
"fluid_ticks": 1800,
|
||||
"log_sha256": "dfedc75558596ce4413bd0466ec03645b2fa6853165510e9b8db0406d0958eb8"
|
||||
},
|
||||
"first_process_diagnostic_sha256": "90872c801cb04e3f0c78b47e0d38a0b45dd5d3e6d1e33ccc53c4359e4b407238",
|
||||
"reload_diagnostic_sha256": "1efe055e1179ce60741a0d8546618807fc136c0de77f9aff6a979cf88b2b1cdd",
|
||||
"fresh_world": {
|
||||
"seed": 42,
|
||||
"process_id": 2892,
|
||||
"generator_version": 12,
|
||||
"islands": [
|
||||
{
|
||||
"upper_biome": "minecraft:jungle",
|
||||
"climate": "tropical",
|
||||
"landing": [
|
||||
0,
|
||||
202,
|
||||
-800
|
||||
],
|
||||
"landing_biome": "minecraft:jungle",
|
||||
"parent": "sanctuary",
|
||||
"id": "alpha",
|
||||
"centre": [
|
||||
0,
|
||||
-800
|
||||
],
|
||||
"diameter": 64
|
||||
},
|
||||
{
|
||||
"upper_biome": "minecraft:desert",
|
||||
"climate": "arid",
|
||||
"landing": [
|
||||
544,
|
||||
198,
|
||||
-800
|
||||
],
|
||||
"landing_biome": "minecraft:desert",
|
||||
"parent": "alpha",
|
||||
"id": "beta",
|
||||
"centre": [
|
||||
544,
|
||||
-800
|
||||
],
|
||||
"diameter": 64
|
||||
}
|
||||
],
|
||||
"scope": "Fresh laboratory world; real FULL chunks and native structure loot, asynchronous activation, branching, journal re-read and loaded void admission refusal. Journal re-read is not a process restart. The parent block witness represents a player edit; no player teleport, multiplayer or client rendering is simulated.",
|
||||
"registered_command_permission_checks": true,
|
||||
"parent_witness": [
|
||||
-16,
|
||||
248,
|
||||
-16
|
||||
],
|
||||
"root_landing": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"journal_reloaded_regions": 3,
|
||||
"visited_void_refusal": {
|
||||
"chunk": [
|
||||
-50,
|
||||
50
|
||||
],
|
||||
"reason": "Expansion intersects a loaded or unloading chunk: [-61, 39]"
|
||||
},
|
||||
"native_structure": {
|
||||
"full_chunks": 12,
|
||||
"placed_block": "Block{minecraft:barrel}[facing=south,open=false]",
|
||||
"structure": "minecraft:abandoned_camp_forest",
|
||||
"mode": "natural candidate accepted by production ExpansionStructures, native decoration",
|
||||
"container": [
|
||||
28,
|
||||
247,
|
||||
73
|
||||
],
|
||||
"loot_table": "minecraft:barrels/abandoned_camp_barrel"
|
||||
},
|
||||
"restart_snapshot": {
|
||||
"world_directory": "<development_game_test_world>",
|
||||
"gold_witness": [
|
||||
544,
|
||||
204,
|
||||
-800
|
||||
],
|
||||
"journal_sha256": "d7de79838744624e98ce26997ddc1daecb7bd3334bd9bf0e1ad31aa5eb7e27c3",
|
||||
"islands": [
|
||||
{
|
||||
"id": "sanctuary",
|
||||
"parent": "sanctuary",
|
||||
"direction": "north",
|
||||
"climate": "temperate",
|
||||
"diameter": 512,
|
||||
"center_x": 0,
|
||||
"center_z": 0,
|
||||
"seed": 42,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "alpha",
|
||||
"parent": "sanctuary",
|
||||
"direction": "north",
|
||||
"climate": "tropical",
|
||||
"diameter": 64,
|
||||
"center_x": 0,
|
||||
"center_z": -800,
|
||||
"seed": 390175152143173586,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "beta",
|
||||
"parent": "alpha",
|
||||
"direction": "east",
|
||||
"climate": "arid",
|
||||
"diameter": 64,
|
||||
"center_x": 544,
|
||||
"center_z": -800,
|
||||
"seed": 6281303726624369227,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
}
|
||||
],
|
||||
"generator_codec": {
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
"minecraft:forest",
|
||||
"minecraft:birch_forest",
|
||||
"minecraft:flower_forest",
|
||||
"minecraft:plains",
|
||||
"minecraft:meadow",
|
||||
"minecraft:taiga",
|
||||
"minecraft:snowy_taiga",
|
||||
"minecraft:snowy_plains",
|
||||
"minecraft:grove",
|
||||
"minecraft:ice_spikes",
|
||||
"minecraft:frozen_peaks",
|
||||
"minecraft:old_growth_spruce_taiga",
|
||||
"minecraft:dark_forest",
|
||||
"minecraft:swamp",
|
||||
"minecraft:pale_garden",
|
||||
"minecraft:jungle",
|
||||
"minecraft:sparse_jungle",
|
||||
"minecraft:bamboo_jungle",
|
||||
"minecraft:mangrove_swamp",
|
||||
"minecraft:savanna",
|
||||
"minecraft:savanna_plateau",
|
||||
"minecraft:desert",
|
||||
"minecraft:badlands",
|
||||
"minecraft:eroded_badlands",
|
||||
"minecraft:wooded_badlands",
|
||||
"minecraft:windswept_hills",
|
||||
"minecraft:windswept_gravelly_hills",
|
||||
"minecraft:stony_peaks",
|
||||
"minecraft:dappled_forest",
|
||||
"minecraft:cherry_grove",
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:sulfur_caves"
|
||||
]
|
||||
},
|
||||
"type": "sanctuary:expansion_lab"
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"stored_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"point": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"id": "sanctuary",
|
||||
"stored_column_sha256": "a291039b2ed502c828b4f1ec97fe111f49f62e69b5589e05116d9b1931f461a4",
|
||||
"resolved_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"base_column_sha256": "646af3da3a327ab15d229b7fe2c328cc10eae7fce31d6b24c60f3be643bc1095",
|
||||
"base_height": 242
|
||||
},
|
||||
{
|
||||
"stored_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"point": [
|
||||
0,
|
||||
202,
|
||||
-800
|
||||
],
|
||||
"id": "alpha",
|
||||
"stored_column_sha256": "0debd009e711898fe64573f31a77f03f03d680be7398918b126004fcee3cfb1c",
|
||||
"resolved_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"base_column_sha256": "9269110205d64e1f72d9c44591a825d66fff3a4278fb835e5b4f2627b01a1db5",
|
||||
"base_height": 202
|
||||
},
|
||||
{
|
||||
"stored_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"point": [
|
||||
544,
|
||||
198,
|
||||
-800
|
||||
],
|
||||
"id": "beta",
|
||||
"stored_column_sha256": "73f7b445475a80b951f05d0c5fb6fd2eca2cf7519cd6ac5d64c7fe9dac53fa23",
|
||||
"resolved_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"base_column_sha256": "4df3dd476f41a8c6b0e0273b9ebe5cbba4dec6cb22001c553c15d4f6b255c555",
|
||||
"base_height": 198
|
||||
}
|
||||
],
|
||||
"world_flushed_before_shutdown": true
|
||||
},
|
||||
"native_structure_starts_seen": 1,
|
||||
"structure_start_chunks_inspected": 106,
|
||||
"ticks": 1390,
|
||||
"elapsed_ms": 43123.278916,
|
||||
"passed": true
|
||||
},
|
||||
"reloaded_world": {
|
||||
"seed": 42,
|
||||
"process_id": 2975,
|
||||
"scope": "A second Java process opens the same saved development chunks and expansion journal, and verifies a gold block written and flushed before the first shutdown. GameTestServer reconstructs its world settings from the preset; the prior generator codec is decoded separately here and its base columns are compared. This is not a normal-client world-selection test or a multiplayer/teleportation test.",
|
||||
"previous_process_id": 2892,
|
||||
"world_directory": "<development_game_test_world>",
|
||||
"gold_witness": [
|
||||
544,
|
||||
204,
|
||||
-800
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"point": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"id": "sanctuary",
|
||||
"stored_column_sha256": "a291039b2ed502c828b4f1ec97fe111f49f62e69b5589e05116d9b1931f461a4",
|
||||
"resolved_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"base_column_sha256": "646af3da3a327ab15d229b7fe2c328cc10eae7fce31d6b24c60f3be643bc1095",
|
||||
"base_height": 242,
|
||||
"stored_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485"
|
||||
},
|
||||
{
|
||||
"point": [
|
||||
0,
|
||||
202,
|
||||
-800
|
||||
],
|
||||
"id": "alpha",
|
||||
"stored_column_sha256": "0debd009e711898fe64573f31a77f03f03d680be7398918b126004fcee3cfb1c",
|
||||
"resolved_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"base_column_sha256": "9269110205d64e1f72d9c44591a825d66fff3a4278fb835e5b4f2627b01a1db5",
|
||||
"base_height": 202,
|
||||
"stored_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b"
|
||||
},
|
||||
{
|
||||
"point": [
|
||||
544,
|
||||
198,
|
||||
-800
|
||||
],
|
||||
"id": "beta",
|
||||
"stored_column_sha256": "73f7b445475a80b951f05d0c5fb6fd2eca2cf7519cd6ac5d64c7fe9dac53fa23",
|
||||
"resolved_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"base_column_sha256": "4df3dd476f41a8c6b0e0273b9ebe5cbba4dec6cb22001c553c15d4f6b255c555",
|
||||
"base_height": 198,
|
||||
"stored_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60"
|
||||
}
|
||||
],
|
||||
"regions": 3,
|
||||
"native_container": [
|
||||
28,
|
||||
247,
|
||||
73
|
||||
],
|
||||
"saved_generator_codec_decoded": true,
|
||||
"passed": true
|
||||
},
|
||||
"earlier_seed_0_attempt": {
|
||||
"scope": "Earlier development run before final cache and error-message fixes. Expansion phases completed; positive loot witness failed.",
|
||||
"native_candidate_sites": 11,
|
||||
"no_surface": 9,
|
||||
"insufficient_foundation": 2,
|
||||
"accepted_structures": 0,
|
||||
"structure_proof_passed": false
|
||||
},
|
||||
"unverified": [
|
||||
"Forced crash during chunk preparation",
|
||||
"Normal client world selection and rendering",
|
||||
"Real player teleports and multiplayer",
|
||||
"Visual quality of every climate/relief and 128/256/512 formats",
|
||||
"Every village and temple variant"
|
||||
]
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.12.1",
|
||||
"date": "2026-09-09",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"java": 25,
|
||||
"generation": 12,
|
||||
"journal_schema": 1,
|
||||
"base_commit": "40a86588b5f83fbc7bd2728eb9b439baa1fc9ba5",
|
||||
"scope": "Development-world engine tests; no personal world, live player teleportation or client rendering. Process timings are not performance benchmarks.",
|
||||
"normalization": "world_directory fields replaced with <development_game_test_world>; other diagnostic fields unchanged.",
|
||||
"build": {
|
||||
"command": "./gradlew check build assemblePack",
|
||||
"passed": true,
|
||||
"required_historical_engine_tests": 12,
|
||||
"seed": 0,
|
||||
"profile": 10,
|
||||
"log_sha256": "72f2173023e5407391d7e0e528012fcda9a314c981acfea462918c36ffa7bf90"
|
||||
},
|
||||
"artifact": {
|
||||
"path": "mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.12.1.jar",
|
||||
"sha256": "c9df4da956cfa54581b3fde21de006bf8ac3ec025b8dd0daa5d060bcb70aabd4"
|
||||
},
|
||||
"first_process_diagnostic_sha256": "3ef35faf09130409135879ad3a4be4ceba4f5644a57ae9cdcec4cd4d6e84c06a",
|
||||
"reload_diagnostic_sha256": "401f2e6739de9efd3668aa27c5ef45a6e81e1acfde44442bf101e5ac6397074a",
|
||||
"fresh_world": {
|
||||
"seed": 42,
|
||||
"process_id": 5743,
|
||||
"generator_version": 12,
|
||||
"islands": [
|
||||
{
|
||||
"centre": [
|
||||
0,
|
||||
-1600
|
||||
],
|
||||
"diameter": 64,
|
||||
"upper_biome": "minecraft:jungle",
|
||||
"climate": "tropical",
|
||||
"landing": [
|
||||
-4,
|
||||
171,
|
||||
-1604
|
||||
],
|
||||
"landing_biome": "minecraft:jungle",
|
||||
"parent": "sanctuary",
|
||||
"id": "alpha"
|
||||
},
|
||||
{
|
||||
"centre": [
|
||||
2048,
|
||||
-1600
|
||||
],
|
||||
"diameter": 64,
|
||||
"upper_biome": "minecraft:desert",
|
||||
"climate": "arid",
|
||||
"landing": [
|
||||
2048,
|
||||
198,
|
||||
-1600
|
||||
],
|
||||
"landing_biome": "minecraft:desert",
|
||||
"parent": "alpha",
|
||||
"id": "beta"
|
||||
}
|
||||
],
|
||||
"scope": "Fresh laboratory world; real FULL chunks and native structure loot, asynchronous activation, branching, journal re-read and loaded void admission refusal. Journal re-read is not a process restart. The parent block witness represents a player edit; no player teleport, multiplayer or client rendering is simulated.",
|
||||
"registered_command_permission_checks": true,
|
||||
"parent_witness": [
|
||||
-16,
|
||||
248,
|
||||
-16
|
||||
],
|
||||
"root_landing": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"initial_occupied_directions": 8,
|
||||
"journal_reloaded_regions": 3,
|
||||
"native_structure": {
|
||||
"container": [
|
||||
28,
|
||||
247,
|
||||
73
|
||||
],
|
||||
"loot_table": "minecraft:barrels/abandoned_camp_barrel",
|
||||
"full_chunks": 12,
|
||||
"placed_block": "Block{minecraft:barrel}[facing=south,open=false]",
|
||||
"structure": "minecraft:abandoned_camp_forest",
|
||||
"mode": "natural candidate accepted by production ExpansionStructures, native decoration"
|
||||
},
|
||||
"restart_snapshot": {
|
||||
"world_directory": "<development_game_test_world>",
|
||||
"gold_witness": [
|
||||
2048,
|
||||
204,
|
||||
-1600
|
||||
],
|
||||
"journal_sha256": "21136a5e8f1857b10ef5673281c8a3a4568ae032641995c206ed2031e112aa5b",
|
||||
"islands": [
|
||||
{
|
||||
"id": "sanctuary",
|
||||
"parent": "sanctuary",
|
||||
"direction": "north",
|
||||
"climate": "temperate",
|
||||
"diameter": 512,
|
||||
"center_x": 0,
|
||||
"center_z": 0,
|
||||
"seed": 42,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "alpha",
|
||||
"parent": "sanctuary",
|
||||
"direction": "north",
|
||||
"climate": "tropical",
|
||||
"diameter": 64,
|
||||
"center_x": 0,
|
||||
"center_z": -1600,
|
||||
"seed": 390175152143173586,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "beta",
|
||||
"parent": "alpha",
|
||||
"direction": "east",
|
||||
"climate": "arid",
|
||||
"diameter": 64,
|
||||
"center_x": 2048,
|
||||
"center_z": -1600,
|
||||
"seed": 6281303726624369227,
|
||||
"relief": "natural",
|
||||
"status": "ready",
|
||||
"generation": 12
|
||||
}
|
||||
],
|
||||
"generator_codec": {
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
"minecraft:forest",
|
||||
"minecraft:birch_forest",
|
||||
"minecraft:flower_forest",
|
||||
"minecraft:plains",
|
||||
"minecraft:meadow",
|
||||
"minecraft:taiga",
|
||||
"minecraft:snowy_taiga",
|
||||
"minecraft:snowy_plains",
|
||||
"minecraft:grove",
|
||||
"minecraft:ice_spikes",
|
||||
"minecraft:frozen_peaks",
|
||||
"minecraft:old_growth_spruce_taiga",
|
||||
"minecraft:dark_forest",
|
||||
"minecraft:swamp",
|
||||
"minecraft:pale_garden",
|
||||
"minecraft:jungle",
|
||||
"minecraft:sparse_jungle",
|
||||
"minecraft:bamboo_jungle",
|
||||
"minecraft:mangrove_swamp",
|
||||
"minecraft:savanna",
|
||||
"minecraft:savanna_plateau",
|
||||
"minecraft:desert",
|
||||
"minecraft:badlands",
|
||||
"minecraft:eroded_badlands",
|
||||
"minecraft:wooded_badlands",
|
||||
"minecraft:windswept_hills",
|
||||
"minecraft:windswept_gravelly_hills",
|
||||
"minecraft:stony_peaks",
|
||||
"minecraft:dappled_forest",
|
||||
"minecraft:cherry_grove",
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:sulfur_caves"
|
||||
]
|
||||
},
|
||||
"type": "sanctuary:expansion_lab"
|
||||
},
|
||||
"blocked_sites": [
|
||||
{
|
||||
"id": "blocked_north",
|
||||
"parent": "sanctuary",
|
||||
"direction": "north",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": 0,
|
||||
"center_z": -800,
|
||||
"seed": 6170446579001265223,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_northeast",
|
||||
"parent": "sanctuary",
|
||||
"direction": "northeast",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": 800,
|
||||
"center_z": -800,
|
||||
"seed": 2312608842580142629,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_east",
|
||||
"parent": "sanctuary",
|
||||
"direction": "east",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": 800,
|
||||
"center_z": 0,
|
||||
"seed": -5785143794938635053,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_southeast",
|
||||
"parent": "sanctuary",
|
||||
"direction": "southeast",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": 800,
|
||||
"center_z": 800,
|
||||
"seed": 6543995692374257629,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_south",
|
||||
"parent": "sanctuary",
|
||||
"direction": "south",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": 0,
|
||||
"center_z": 800,
|
||||
"seed": 9109857537375437182,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_southwest",
|
||||
"parent": "sanctuary",
|
||||
"direction": "southwest",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": -800,
|
||||
"center_z": 800,
|
||||
"seed": 7977138842745944844,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_west",
|
||||
"parent": "sanctuary",
|
||||
"direction": "west",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": -800,
|
||||
"center_z": 0,
|
||||
"seed": 8731833603567083481,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
},
|
||||
{
|
||||
"id": "blocked_northwest",
|
||||
"parent": "sanctuary",
|
||||
"direction": "northwest",
|
||||
"climate": "boreal",
|
||||
"diameter": 64,
|
||||
"center_x": -800,
|
||||
"center_z": -800,
|
||||
"seed": 6470159394492078415,
|
||||
"relief": "natural",
|
||||
"status": "reserved",
|
||||
"generation": 12
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"resolved_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"base_column_sha256": "646af3da3a327ab15d229b7fe2c328cc10eae7fce31d6b24c60f3be643bc1095",
|
||||
"base_height": 242,
|
||||
"stored_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"point": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"id": "sanctuary",
|
||||
"stored_column_sha256": "a291039b2ed502c828b4f1ec97fe111f49f62e69b5589e05116d9b1931f461a4"
|
||||
},
|
||||
{
|
||||
"resolved_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"base_column_sha256": "4ec4238d357426e34a1decfd817fc96c6b596758e98ffc1d98906a1db8d5b707",
|
||||
"base_height": 204,
|
||||
"stored_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"point": [
|
||||
-4,
|
||||
171,
|
||||
-1604
|
||||
],
|
||||
"id": "alpha",
|
||||
"stored_column_sha256": "9866d275b53719e434d20996e2c061f4342e431c41d671216781d1f00c204ff1"
|
||||
},
|
||||
{
|
||||
"resolved_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"base_column_sha256": "4df3dd476f41a8c6b0e0273b9ebe5cbba4dec6cb22001c553c15d4f6b255c555",
|
||||
"base_height": 198,
|
||||
"stored_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"point": [
|
||||
2048,
|
||||
198,
|
||||
-1600
|
||||
],
|
||||
"id": "beta",
|
||||
"stored_column_sha256": "246bc9b1827effb58159dbc540c655f2d218079a49263d19ac786cf116557093"
|
||||
}
|
||||
],
|
||||
"world_flushed_before_shutdown": true
|
||||
},
|
||||
"native_structure_starts_seen": 1,
|
||||
"structure_start_chunks_inspected": 106,
|
||||
"ticks": 1470,
|
||||
"elapsed_ms": 41563.165541,
|
||||
"passed": true
|
||||
},
|
||||
"reloaded_world": {
|
||||
"seed": 42,
|
||||
"process_id": 5830,
|
||||
"scope": "A second Java process opens the same saved development chunks and expansion journal, and verifies a gold block written and flushed before the first shutdown. GameTestServer reconstructs its world settings from the preset; the prior generator codec is decoded separately here and its base columns are compared. This is not a normal-client world-selection test or a multiplayer/teleportation test.",
|
||||
"saved_only_collision_refusals": 8,
|
||||
"previous_process_id": 5743,
|
||||
"world_directory": "<development_game_test_world>",
|
||||
"gold_witness": [
|
||||
2048,
|
||||
204,
|
||||
-1600
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"stored_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"point": [
|
||||
-16,
|
||||
242,
|
||||
-16
|
||||
],
|
||||
"id": "sanctuary",
|
||||
"stored_column_sha256": "a291039b2ed502c828b4f1ec97fe111f49f62e69b5589e05116d9b1931f461a4",
|
||||
"resolved_biomes_sha256": "65db6d2cde63dee7b60bc74aec702aae61f558c3878b8c8efaf0cc75d6066485",
|
||||
"base_column_sha256": "646af3da3a327ab15d229b7fe2c328cc10eae7fce31d6b24c60f3be643bc1095",
|
||||
"base_height": 242
|
||||
},
|
||||
{
|
||||
"stored_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"point": [
|
||||
-4,
|
||||
171,
|
||||
-1604
|
||||
],
|
||||
"id": "alpha",
|
||||
"stored_column_sha256": "9866d275b53719e434d20996e2c061f4342e431c41d671216781d1f00c204ff1",
|
||||
"resolved_biomes_sha256": "dfda92ebd5677b8ea4fcd3b02ba029a898b22a8f640a71b520bab9783695eb1b",
|
||||
"base_column_sha256": "4ec4238d357426e34a1decfd817fc96c6b596758e98ffc1d98906a1db8d5b707",
|
||||
"base_height": 204
|
||||
},
|
||||
{
|
||||
"stored_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"point": [
|
||||
2048,
|
||||
198,
|
||||
-1600
|
||||
],
|
||||
"id": "beta",
|
||||
"stored_column_sha256": "246bc9b1827effb58159dbc540c655f2d218079a49263d19ac786cf116557093",
|
||||
"resolved_biomes_sha256": "7233e9fb21f196866122679eec5cabf39d383b8a26fff0848ff6700d1c9fab60",
|
||||
"base_column_sha256": "4df3dd476f41a8c6b0e0273b9ebe5cbba4dec6cb22001c553c15d4f6b255c555",
|
||||
"base_height": 198
|
||||
}
|
||||
],
|
||||
"regions": 3,
|
||||
"native_container": [
|
||||
28,
|
||||
247,
|
||||
73
|
||||
],
|
||||
"saved_generator_codec_decoded": true,
|
||||
"after_reload_expansion": [
|
||||
0,
|
||||
1600
|
||||
],
|
||||
"after_reload_landing": [
|
||||
-4,
|
||||
153,
|
||||
1596
|
||||
],
|
||||
"final_regions": 4,
|
||||
"passed": true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,115 +0,0 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.13.1",
|
||||
"date": "2026-09-09",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"java": 25,
|
||||
"host_os": "macOS",
|
||||
"windows_native_tested": false,
|
||||
"windows_policy_tested_by_injection": true,
|
||||
"journal_contracts": [
|
||||
"Windows policy skips only directory channel",
|
||||
"file force and atomic replacement and strict read-back remain required",
|
||||
"supported directory AccessDenied propagates after replacement",
|
||||
"failed session refuses further writes",
|
||||
"verified reload recovers persisted reservation",
|
||||
"post-replacement corruption fails closed"
|
||||
],
|
||||
"jar_sha256": "b340dfc58f3d2d1a2a375dc527ac6721c61430600622046db26228e0da46061b",
|
||||
"logs": {
|
||||
"journal-smoke": {
|
||||
"path": "build/alpha131-journal-smoke.log",
|
||||
"sha256": "c02e94713cf3f9a3c76674700cc5c800aecd84b213b2be9ed53629f8eb9cd3d0",
|
||||
"passed": true,
|
||||
"required_game_tests": null
|
||||
},
|
||||
"check": {
|
||||
"path": "build/alpha131-check.log",
|
||||
"sha256": "68f6cb97e2f80669d39e37623076229e030a0d9826c13e9420f51f6ba23885bd",
|
||||
"passed": true,
|
||||
"required_game_tests": 8
|
||||
},
|
||||
"expansion-create": {
|
||||
"path": "build/alpha131-expansion-create.log",
|
||||
"sha256": "61fa4513aef71290a1419c3425a329047c8fe623a561544c6a8d916fe50e226c",
|
||||
"passed": true,
|
||||
"required_game_tests": 2
|
||||
},
|
||||
"expansion-reload": {
|
||||
"path": "build/alpha131-expansion-reload.log",
|
||||
"sha256": "44fafb4636ff90707a068f325a245dbf9820ccb1bdf785c912f7e636fe7cfa43",
|
||||
"passed": true,
|
||||
"required_game_tests": 2
|
||||
}
|
||||
},
|
||||
"binary_comparison": {
|
||||
"base_version": "0.1.0-alpha.13",
|
||||
"version": "0.1.0-alpha.13.1",
|
||||
"added": [
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal$DirectorySync.class"
|
||||
],
|
||||
"removed": [],
|
||||
"changed": [
|
||||
"fabric.mod.json",
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal$1.class",
|
||||
"fr/koka/sanctuary/expansion/ExpansionJournal.class"
|
||||
],
|
||||
"synthetic_switch_instructions_identical": true,
|
||||
"all_other_entries_identical": true
|
||||
},
|
||||
"expansion": {
|
||||
"seed": 42,
|
||||
"initial_diameter": 724,
|
||||
"generation": 13,
|
||||
"creation_process": 13148,
|
||||
"reload_process": 13279,
|
||||
"creation_passed": true,
|
||||
"reload_passed": true,
|
||||
"creation_islands": [
|
||||
{
|
||||
"landing_biome": "minecraft:jungle",
|
||||
"landing": [
|
||||
0,
|
||||
202,
|
||||
-1792
|
||||
],
|
||||
"climate": "tropical",
|
||||
"upper_biome": "minecraft:jungle",
|
||||
"diameter": 64,
|
||||
"centre": [
|
||||
0,
|
||||
-1792
|
||||
],
|
||||
"id": "alpha",
|
||||
"parent": "sanctuary"
|
||||
},
|
||||
{
|
||||
"landing_biome": "minecraft:desert",
|
||||
"landing": [
|
||||
2048,
|
||||
198,
|
||||
-1792
|
||||
],
|
||||
"climate": "arid",
|
||||
"upper_biome": "minecraft:desert",
|
||||
"diameter": 64,
|
||||
"centre": [
|
||||
2048,
|
||||
-1792
|
||||
],
|
||||
"id": "beta",
|
||||
"parent": "alpha"
|
||||
}
|
||||
],
|
||||
"reloaded_regions": 3,
|
||||
"saved_generator_codec_decoded": true,
|
||||
"after_reload_expansion": [
|
||||
0,
|
||||
1792
|
||||
],
|
||||
"final_regions": 4,
|
||||
"native_structure_status": "not_observed_after_complete_scan",
|
||||
"native_structure_witness_required": false,
|
||||
"limitation": "Optional native camp witness absent after complete search on seed 42, as in alpha.13; no new structures claim."
|
||||
},
|
||||
"scope": "No generation or journal schema change. Test worlds only; no personal world opened or regenerated. Windows native runtime and power-loss durability not tested."
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
# Blocodex natif — connaissances, matière et stocks
|
||||
|
||||
Ticket META-01, branche `codex/blocodex-natif`, cible Minecraft **26.3-pre-2**.
|
||||
Le Blocodex appartient au mod Sanctuary. Il est disponible sans appareil,
|
||||
recette ni déblocage. La mémoire personnelle, le recensement du terrain et
|
||||
celui des stocks forment un socle commun aux futures palettes de structures,
|
||||
règles d’expansion et règles du shop. Les relevés datés ajoutent la temporalité
|
||||
nécessaire à une future lecture de l’histoire économique du serveur.
|
||||
|
||||
## Contrat des informations
|
||||
|
||||
La mémoire est personnelle : une identité de joueur (UUID), dans une sauvegarde
|
||||
de serveur, partagée entre ses dimensions. Les indicateurs sont indépendants.
|
||||
|
||||
| Information | Sens |
|
||||
| --- | --- |
|
||||
| Inconnu | Aucune observation, possession, pose ni statistique de minage ou de jet. C'est un état calculé, pas une étape enregistrée. |
|
||||
| Vu | Le premier bloc rencontré par le regard du joueur, à au plus 32 blocs, observé côté serveur tous les 5 ticks. Les obstacles arrêtent le rayon ; les chunks absents ne sont pas chargés. Ce relevé ne couvre pas chaque pixel du champ de vision. |
|
||||
| Miné | Compteur vanilla `mined` du bloc. Ses règles sont conservées : notamment pas de minage créatif, ni de minage sans l'outil requis. |
|
||||
| Déjà possédé | Le bloc, sous forme d'item, a été présent dans l'inventaire personnel ou sur le curseur d'un menu. Les statistiques vanilla de ramassage et de jet prouvent aussi une possession passée. |
|
||||
| Jeté | Compteur vanilla `dropped` de l'item du bloc. Il compte les unités volontairement jetées, pas les objets éjectés à la mort ni le butin produit par le bloc cassé. |
|
||||
| Posé | Nombre de placements réussis par le joueur avec un item de bloc, y compris en créatif. Une tentative refusée, une interaction ou un placement par commande ne compte pas. |
|
||||
| En inventaire | Quantité actuellement dans l'inventaire personnel, équipement et curseur compris. Les coffres ouverts, ender chests et contenus imbriqués des boîtes ne sont pas inclus. |
|
||||
|
||||
Les blocs sont identifiés par leur identifiant de registre ; les propriétés
|
||||
d'état (orientation, humidité, etc.) ne créent pas une nouvelle fiche. Deux
|
||||
identifiants restent distincts : une torche murale peut être vue et posée tandis
|
||||
que l'item possédé correspond à la torche debout. Les noms des blocs inconnus
|
||||
restent consultables, comme un catalogue ; cette version ne décide pas d'une
|
||||
politique de secrets ou de verrouillage des recettes.
|
||||
|
||||
L'inventaire est observé chaque tick serveur, après les transactions de menus
|
||||
et avant la consommation d'un item placé. Les statistiques de ramassage/jet
|
||||
complètent ce relevé. Une mutation d'inventaire entièrement réalisée entre ces
|
||||
points par un autre mod ne peut pas être reconstruite après coup.
|
||||
Les spectateurs ne découvrent pas de blocs par le regard.
|
||||
|
||||
## Persistance et arrivée dans une sauvegarde antérieure
|
||||
|
||||
Contrat additif v1 : fichiers `data/sanctuary-blocodex/<uuid>.json`, enveloppe
|
||||
avec `schema: 1` et UUID, puis observations, possessions et poses par identifiant.
|
||||
Les statistiques vanilla restent dans leurs propres fichiers et sont seulement
|
||||
lues. Aucune conversion du terrain, des paramètres de génération, des chunks,
|
||||
du journal d'expansion ou des statistiques existantes n'est effectuée.
|
||||
|
||||
Sans fichier Blocodex, les observations et poses commencent vides. Le minage,
|
||||
le jet et les preuves de possession sont disponibles depuis les statistiques
|
||||
vanilla déjà présentes. On n'invente pas les observations ni les poses passées
|
||||
à partir de `used`, qui compte aussi d'autres usages.
|
||||
|
||||
Les changements sont sauvegardés tous les 600 ticks, lors de la sauvegarde du
|
||||
serveur, à la déconnexion et à l'arrêt. Un crash peut perdre les dernières
|
||||
observations ou poses non sauvegardées. Un fichier invalide ou d'une version
|
||||
inconnue désactive la mémoire concernée et n'est jamais remplacé silencieusement.
|
||||
Les identifiants de mods absents sont conservés sur disque. Les anciennes
|
||||
versions de Sanctuary ignorent ce répertoire additionnel ; les poses réalisées
|
||||
pendant un retour à une ancienne version ne peuvent pas être récupérées.
|
||||
|
||||
## Réutilisation
|
||||
|
||||
`BlockKnowledgeService.knowledge(player, block)` et `snapshot(player)` renvoient
|
||||
des valeurs immuables calculées côté serveur. Le service expose aussi sa santé ;
|
||||
un consommateur doit refuser une condition de progression si la mémoire est
|
||||
indisponible, plutôt que d'interpréter une panne comme une découverte vierge.
|
||||
`BlockKnowledgeEvents.CHANGED` permet de réagir aux observations, possessions,
|
||||
poses et changements de statistiques. Les écouteurs s'exécutent sur le thread
|
||||
serveur ; ils ne constituent pas un journal transactionnel de récompenses.
|
||||
|
||||
Cette livraison ne verrouille aucune recette et n'accorde aucune récompense.
|
||||
Palettes disponibles, recherches, progression et structures pourront ensuite
|
||||
définir leurs propres conditions en utilisant cette API commune.
|
||||
|
||||
### Palettes de structures
|
||||
|
||||
`BlockKnowledgeService.palette(player, condition, tag)` croise les connaissances
|
||||
du joueur avec une famille de matériaux. Les critères `BlockKnowledgeFacet`
|
||||
sont combinables avec `and` et `or` : vus **ou** déjà possédés, minés **et**
|
||||
actuellement portés, etc. Le résultat immuable garde les identifiants triés et
|
||||
les quantités réellement portées.
|
||||
|
||||
```java
|
||||
var candidates = BlockKnowledgeService.palette(player,
|
||||
BlockKnowledgeFacet.SEEN.or(BlockKnowledgeFacet.POSSESSED),
|
||||
BlockKnowledgeService.NATURAL_STRUCTURE_MATERIALS);
|
||||
```
|
||||
|
||||
Le tag `sanctuary:structure_palette/natural` fournit une première famille de
|
||||
pierres, terres, bois, feuilles et autres matériaux naturels, modifiable par
|
||||
datapack. D'autres tags pourront définir des palettes minérales, végétales ou
|
||||
propres à un biome. Le choix des couleurs, les contraintes physiques des blocs,
|
||||
les quantités nécessaires et la construction de la structure appartiendront au
|
||||
moteur de structures : une sélection de palette ne réserve aucun stock et ne
|
||||
place aucun bloc.
|
||||
|
||||
## Recensement du terrain chargé
|
||||
|
||||
`TerrainCensus.snapshot(server)` expose une distribution des blocs par dimension ;
|
||||
`TerrainCensus.chunkSnapshot(level, pos)` fournit le détail d’un chunk déjà
|
||||
chargé. Ces lectures se font sur le thread serveur et renvoient des valeurs
|
||||
immuables. Elles parcourent les chunks `FULL` déjà accessibles, sans charger
|
||||
de chunk absent ni attendre la fin d’une génération.
|
||||
|
||||
Le comptage utilise les palettes des sections de 16 × 16 × 16 blocs. Il regroupe
|
||||
les orientations et autres propriétés sous l’identifiant du bloc. Les blocs
|
||||
d’air sont exclus ; l’eau et la lave conservent leurs identifiants de blocs.
|
||||
Le budget est de **16 sections non vides par tick serveur**, partagé entre les
|
||||
dimensions et les demandes du même tick. Une section entièrement vide est
|
||||
reconnue par ses métadonnées, sans parcourir sa palette.
|
||||
|
||||
Le relevé est progressif. Une section modifiée est retirée des totaux dès que
|
||||
sa révision change, puis remise dans la file de comptage. Les sections et
|
||||
chunks déchargés sortent du relevé. Un consommateur dispose, par dimension ou
|
||||
chunk, du nombre de sections chargées et comptées, du tick de capture, du tick
|
||||
du dernier comptage effectué et de l’âge de la plus ancienne attente.
|
||||
`complete()` et `coverage()` décrivent la couverture **des zones chargées**.
|
||||
Une couverture de 100 % ne signifie jamais que toute la sauvegarde a été lue.
|
||||
|
||||
Pendant un rattrapage, zéro bloc compté ne prouve donc pas l’absence de ce bloc.
|
||||
Les futures règles d’expansion devront fixer leur couverture minimale et leur
|
||||
zone d’étude avant de prendre une décision. Les résultats en mémoire sont
|
||||
reconstruits après redémarrage ; ce service n’écrit pas dans les chunks et ne
|
||||
modifie pas leur génération.
|
||||
|
||||
## Recensement des stocks présents
|
||||
|
||||
`StockCensus.snapshot(server)` photographie les objets accessibles au tick de
|
||||
la demande, sur le thread serveur. Le parcours des slots et des entités chargées
|
||||
est effectué à la demande ; aucun abonnement ne le relance à chaque tick.
|
||||
Les quantités sont des unités d’items, tous
|
||||
types confondus : un lingot, un outil ou une graine sont comptés même s’ils ne
|
||||
correspondent pas à un bloc. Les composants, l’usure et les enchantements ne
|
||||
créent pas de nouveaux identifiants de stock dans cette version.
|
||||
|
||||
| Catégorie | Couverture |
|
||||
| --- | --- |
|
||||
| `playerItems` | Joueurs connectés : inventaire personnel, équipement et curseur du menu. Le contenu du menu ouvert n’est pas additionné. |
|
||||
| `blockContainerItems` | Entités de blocs déjà instanciées dans les chunks chargés, qui exposent l’interface `Container` : coffres, hoppers, fours, pots décorés, etc. Chaque moitié d’un double coffre est comptée une fois. |
|
||||
| `entityContainerItems` | Entités chargées qui exposent `Container`, notamment les wagonnets et bateaux à coffre. |
|
||||
| `groundItems` | Quantités des `ItemEntity` chargées et encore présentes au sol. |
|
||||
|
||||
Le relevé ne déballe **aucune table de butin**. Tout conteneur dont la table
|
||||
reste à résoudre est ignoré avant la lecture de ses slots ; son existence
|
||||
augmente `pendingLootContainers`. Les entités de blocs encore stockées sous
|
||||
forme de NBT et non instanciées sont également laissées intactes, avec leur
|
||||
nombre dans `pendingBlockEntities`. Leur contenu n’est pas remplacé par une
|
||||
estimation.
|
||||
|
||||
La couverture exclut les joueurs hors ligne, les chunks et entités déchargés,
|
||||
les ender chests, le contenu imbriqué des shulker boxes et bundles, les grilles
|
||||
de fabrication en cours dans l’inventaire ou l’établi (`menuInputsIncluded()`
|
||||
est faux), les inventaires et équipements des créatures. Le résultat de
|
||||
fabrication affiché avant validation ne constitue pas un stock supplémentaire.
|
||||
Elle exclut aussi les objets internes
|
||||
des entités de blocs qui n’exposent pas `Container`, comme les aliments d’un feu
|
||||
de camp ou le livre d’un pupitre. Une shulker box portée compte comme un item ;
|
||||
une shulker box posée et chargée expose son conteneur. Les exclusions figurent
|
||||
dans le contrat de couverture ; `partial()` reste vrai.
|
||||
|
||||
Ce relevé indique où se trouvent les unités observées. Il ne les réserve pas,
|
||||
ne les transfère pas et n’évalue pas leur prix. Ajouter le terrain aux stocks
|
||||
ne donne pas une quantité immédiatement vendable : une roche dans un chunk
|
||||
reste différente d’un item dans un coffre.
|
||||
|
||||
## Consultation et futurs consommateurs communs
|
||||
|
||||
`WorldCensusService.capture(server)` rassemble les deux relevés pour une même
|
||||
lecture côté serveur. Le consommateur réutilise cet instantané pendant son
|
||||
calcul et conserve ses informations de couverture. Le shop et les expansions
|
||||
pourront ainsi consulter la même base, avec leurs propres règles de fraîcheur,
|
||||
de portée géographique, de prix, de consommation et de réservation.
|
||||
|
||||
Les diagnostics suivants sont réservés aux opérateurs :
|
||||
|
||||
```text
|
||||
/sanctuary census
|
||||
/sanctuary census block minecraft:stone
|
||||
/sanctuary census item minecraft:gold_ingot
|
||||
```
|
||||
|
||||
Le résumé présente la couverture du terrain par dimension, les quatre catégories
|
||||
de stocks et les conteneurs au butin encore indéterminé. Le Blocodex personnel
|
||||
accessible par **B** conserve les données du joueur ; ces diagnostics ne diffusent
|
||||
pas les inventaires agrégés aux autres joueurs.
|
||||
|
||||
## Relevés datés — portée de l’alpha.23
|
||||
|
||||
La collecte regroupe les activités observées par **jour civil UTC**, pour
|
||||
l’ensemble du serveur, sans attribution à un UUID ou à une dimension :
|
||||
|
||||
| Action | Registre et unité |
|
||||
| --- | --- |
|
||||
| `mined` | `block` : nombre de blocs minés, selon les règles des statistiques vanilla. |
|
||||
| `placed` | `block` : placements réussis par le joueur, créatif inclus et spectateurs exclus. |
|
||||
| `picked_up` | `item` : unités ramassées observées par les statistiques vanilla. |
|
||||
| `dropped` | `item` : unités volontairement jetées observées par les statistiques vanilla. |
|
||||
| `crafted` | `item` : unités fabriquées créditées au joueur par les statistiques vanilla. |
|
||||
|
||||
Chaque combinaison conserve son action, son registre, son identifiant et une
|
||||
quantité entière sur 64 bits, saturée à la valeur maximale en cas de dépassement.
|
||||
Un compteur de minage n’est pas transformé en nombre d’items produits. Les actions
|
||||
antérieures à l’installation ne sont pas réparties artificiellement entre des
|
||||
dates à partir des statistiques cumulées de Minecraft.
|
||||
|
||||
`MaterialActivityHistory.snapshot(server, day)` expose un instantané immuable
|
||||
avec le jour, les compteurs, `recorded` et `unsaved`. Un jour sans données
|
||||
enregistrées demeure inconnu ; il ne constitue pas un historique d’activité nulle.
|
||||
Les opérateurs peuvent consulter `/sanctuary history` ou préciser une date avec
|
||||
`/sanctuary history 2026-09-10`. La commande affiche au plus les 16 lignes aux
|
||||
quantités les plus élevées et indique combien ont été omises ; l’API conserve
|
||||
l’ensemble des compteurs du jour.
|
||||
|
||||
La persistance additionnelle utilise `data/sanctuary-activity/YYYY-MM-DD.json`,
|
||||
avec un schéma 1 et des dates UTC. Elle est séparée de la mémoire personnelle
|
||||
et des chunks. Les écritures ont lieu tous les **600 ticks**, à la sauvegarde
|
||||
et à l’arrêt du serveur, ainsi qu’au changement de jour observé par le service.
|
||||
Un crash peut perdre les derniers relevés non sauvegardés, soit normalement
|
||||
jusqu’à 30 secondes de jeu à 20 ticks par seconde. Le fichier conserve les
|
||||
identifiants de mods absents ; il refuse les schémas inconnus, doublons,
|
||||
compteurs invalides et changements externes. Une erreur arrête la collecte et
|
||||
rend le service indisponible jusqu’à réparation puis redémarrage, en conservant
|
||||
le fichier en cause. Les limites sont de 100 000 tuples et 16 Mio par jour.
|
||||
|
||||
Ces relevés permettent de distinguer des périodes d’activité. Ils ne conservent
|
||||
pas d’historique des instantanés de stocks, ne couvrent pas la fabrication automatique des
|
||||
machines et n’établissent ni consommation, ni perte, ni destination des objets.
|
||||
Ils ne retracent pas chaque transfert entre coffres ou chaque changement de propriétaire, et
|
||||
ne constituent pas un journal transactionnel d’achats, de récompenses ou de
|
||||
réservations. Le **ballast**, ses unités, ses conversions et sa traduction en
|
||||
géologie restent à concevoir dans le [cahier de cosmologie](cosmologie.md).
|
||||
Aucun objet n’est consommé et aucune Backroom n’est générée par ces mesures.
|
||||
|
||||
## Référence WSMC retrouvée
|
||||
|
||||
La recherche locale a retrouvé une partie du travail précédent dans
|
||||
`/Users/koka/Documents/sanctuary/Gameplay/blocodex` :
|
||||
`ClientVoxelProjectBuilder` convertit des apparences de blocs et d’items en
|
||||
volumes/couches de blocs ; `BlocodexStudioPalettes` sélectionne des matériaux
|
||||
opaques appris. Le projet conserve aussi un instantané WSMC de 1 881 entrées
|
||||
(1 168 blocs, 490 items, 223 mobs), issu de Minecraft 1.21.11.
|
||||
|
||||
Cette référence reste historique. La nouvelle API fournit la connaissance et
|
||||
les candidats de palette ; la traduction de modèles d’entités en structures
|
||||
(visage de creeper, squelette géant, etc.) reste un chantier distinct. Aucun
|
||||
générateur d’entités ni import de ce snapshot ancien n’est ajouté au registre
|
||||
de Minecraft 26.3 par le socle méta.
|
||||
|
||||
## Validation
|
||||
|
||||
Validation locale du 10 septembre 2026 : `./gradlew check build assemblePack`
|
||||
passe, avec **24 GameTests natifs réussis**. Le client graphique vérifie les
|
||||
widgets et les messages réels dans un nouveau monde intégré, avec B sans droit
|
||||
opérateur, observation serveur et possession conservée après retrait du stock.
|
||||
Les tests de stockage passent. Les [preuves et limites](testing-alpha23.md)
|
||||
détaillent le protocole, l’artefact et son empreinte. Alpha.23 assemblée localement ;
|
||||
publication du MRpack et mise à jour du canal demandées, en préparation.
|
||||
@@ -1,275 +0,0 @@
|
||||
# Sanctuary — matière, espaces et mémoire
|
||||
|
||||
**Cahier de conception issu du texte de l’auteur.** Il prolonge
|
||||
[la vision](vision.md) et [la conception des lieux](structures-conception.md).
|
||||
Les citations conservent les formulations fournies ; les points à préciser
|
||||
sont identifiés comme tels. Ce document ne livre aucun générateur ni système
|
||||
économique. **Backrooms, Indoors, shop et ballast ne sont pas implémentés.**
|
||||
Le Blocodex natif de l’alpha.23 locale fournit un premier socle de données
|
||||
pour des mécaniques futures, avec les limites décrites dans [son contrat](blocodex.md).
|
||||
|
||||
## Minecraft comme archéologie d’un autre Minecraft
|
||||
|
||||
Steve, Ari, Sunny, Kai, Zuri, Alex, Mefe, Makena et Noor sont les neuf personnages
|
||||
disparus de la partie. Ils ont découvert quelque chose de révolutionnaire et ont
|
||||
continué parce que ça marchait. Jusqu’au moment où ça n’a plus marché.
|
||||
|
||||
> Ils n’ont pas ouvert quelque chose qu’ils n’auraient pas dû ouvrir. Ils ont
|
||||
> construit quelque chose qu’ils ne savaient plus refermer.
|
||||
|
||||
L’histoire part de quelque chose qui a fonctionné : des besoins satisfaits, des
|
||||
lieux devenus utilisables, des opérations répétées et des installations agrandies.
|
||||
Une ruine doit permettre de retrouver cette ancienne solution. Ses circulations,
|
||||
ses raccords, ses réparations et ses extensions racontent un fonctionnement avant
|
||||
de raconter son interruption.
|
||||
|
||||
> Chaque ruine de sanctuary doit être une trace d’une ancienne solution pas
|
||||
> seulement la trace d’un problème.
|
||||
|
||||
La découverte précise, la succession des expériences et les rôles des neuf
|
||||
personnages restent à écrire. Aucun métier, aucune responsabilité individuelle
|
||||
ni cause définitive de leur disparition ne leur sont attribués ici. On conserve
|
||||
en particulier **Mefe**, tel que nommé par l’auteur. Il n’est pas décidé que
|
||||
chacun possède un bâtiment, une machine, une gemme ou une quête correspondante.
|
||||
|
||||
## Deux devenirs de la même technologie
|
||||
|
||||
Les Indoors et les Backrooms appartiennent à la même logique de fabrication des
|
||||
espaces. Ce qui les distingue est leur relation à une intention et à une
|
||||
référence : sait-on encore pourquoi cet espace existe et comment le retrouver ?
|
||||
|
||||
| Espace | Direction donnée par l’auteur | Conséquence pour la conception |
|
||||
| --- | --- | --- |
|
||||
| **Indoor** | « espace artificiel référencé et contrôlé » | Un intérieur est produit intentionnellement et reste associé à son accès et à son usage. Une porte peut mener à un espace dix fois plus grand que le bâtiment extérieur. |
|
||||
| **Backroom** | « espace artificiel orphelin » | Un espace demeure alors que sa destination ou sa raison d’être a disparu. Sa forme peut conserver les restes d’une fonction sans retrouver l’ensemble auquel elle appartenait. |
|
||||
|
||||
L’auteur décrit chaque Indoor comme une petite dimension instanciée. Les
|
||||
computers utilisent **probablement** cette technologie pour leurs expériences :
|
||||
cette relation est une piste forte, sans avoir encore de protocole ni de machine
|
||||
définitivement assignée.
|
||||
|
||||
Les Backrooms, dans cette cosmologie, ne sont pas une destination demandée au
|
||||
même titre qu’un monde. Elles sont ce que la machine produit lorsqu’elle ne sait
|
||||
pas où mettre quelque chose :
|
||||
|
||||
- un espace généré puis annulé ;
|
||||
- une expérience interrompue ;
|
||||
- deux règles demandant deux états incompatibles ;
|
||||
- une room qui n’est plus référencée ;
|
||||
- une coordonnée qu’aucun monde ne revendique.
|
||||
|
||||
> La machine ne détruit pas parfaitement les espaces. Elle les laisse derrière.
|
||||
> Backrooms.
|
||||
|
||||
L’image du « garbage collector raté de Minecraft » exprime cette accumulation.
|
||||
L’arborescence proposée par l’auteur la représente ainsi :
|
||||
|
||||
```text
|
||||
WORLD
|
||||
├── overworld
|
||||
├── caverns
|
||||
├── nether
|
||||
├── end
|
||||
├── sanctuary
|
||||
└── /unreferenced/
|
||||
└── backrooms
|
||||
```
|
||||
|
||||
C’est une représentation narrative, pas une arborescence de fichiers ni la
|
||||
liste des dimensions actuellement enregistrées par le mod. **L’espace orphelin
|
||||
est une fiction à mettre en scène.** Une réalisation Minecraft devra conserver
|
||||
des espaces correctement sauvegardés, identifiés et référencés côté serveur.
|
||||
Le choix entre dimensions techniques et régions d’un espace partagé reste à
|
||||
concevoir. Rien ici ne prévoit de perdre de vraies références de sauvegarde,
|
||||
de supprimer des chunks ou de provoquer leur corruption pour obtenir cet effet.
|
||||
|
||||
## De la matière sans contexte
|
||||
|
||||
Le système reçoit des matériaux et tente de les réorganiser en espace valide :
|
||||
|
||||
```text
|
||||
stone stone dirt copper oak_planks iron stone glass ...
|
||||
```
|
||||
|
||||
Il possède de la matière, mais aucune destination n’a été demandée. Il tente
|
||||
alors de l’encoder sous forme de chunks : un couloir, une salle, encore une
|
||||
salle, un plafond, une porte donnant sur rien, une plomberie sans bâtiment,
|
||||
un entrepôt infini.
|
||||
|
||||
> Les Backrooms sont la tentative du générateur de Minecraft de faire quelque
|
||||
> chose avec de la matière sans contexte.
|
||||
|
||||
La **matière** donne une palette et une présence physique. Le **contexte** lui
|
||||
donnait un usage, des dimensions, des voisins, un accès ou une destination.
|
||||
Cette distinction permet de dessiner un lieu dont les parties sont intelligibles
|
||||
alors que leur assemblage ne retrouve plus son intention : une plomberie garde
|
||||
sa logique de raccord, mais le bâtiment qu’elle devait desservir n’est plus là.
|
||||
Elle ne justifie pas de remplacer la conception par des blocs placés au hasard.
|
||||
|
||||
Il reste à définir ce que la machine conserve de ce contexte : seulement les
|
||||
matériaux, des fragments de plans, des types de pièces, des relations entre
|
||||
espaces ou des traces d’opérations. Aucun catalogue de rooms ni algorithme de
|
||||
recomposition n’est arrêté. Les palettes limitées et le futur moteur de
|
||||
traduction en structures peuvent servir cette direction ; le moteur n’est pas
|
||||
créé par ce document.
|
||||
|
||||
## Le ballast de l’économie
|
||||
|
||||
La direction fournie relie directement cette cosmologie aux actions des joueurs.
|
||||
Utiliser le shop, téléporter, ouvrir des Indoors, déplacer de la matière ou
|
||||
expandre le monde produit du **ballast**. Plus ces opérations se multiplient,
|
||||
plus les Backrooms se remplissent. Elles deviennent l’externalité du système
|
||||
économique de Sanctuary : une activité visible laisse ailleurs des conséquences
|
||||
que ses utilisateurs pourront découvrir beaucoup plus tard.
|
||||
|
||||
Le ballast relie donc production, échanges et géographie. Un objet déplacé ne
|
||||
change pas seulement de propriétaire ou de position dans cette vision ; son
|
||||
passage par ces technologies peut laisser une empreinte dans le monde.
|
||||
La forme de cette empreinte reste à décider pour chaque opération.
|
||||
|
||||
| Opération envisagée | Articulation à définir avant de l’implémenter |
|
||||
| --- | --- |
|
||||
| Shop et échanges | Quel événement produit du ballast : vente, achat, consommation par un service, autre opération ? Quelle matière lui correspond et à quel moment l’échange est-il terminé ? |
|
||||
| Téléportation | Que laisse le déplacement : trace de matière transportée, coût matériel ou autre résidu à choisir ? Les types de téléportation partagent-ils cette règle ? |
|
||||
| Ouverture d’un Indoor | Que produit la création d’un espace contrôlé ? Que deviennent ensuite une fermeture, un transfert d’accès ou une expérience interrompue ? |
|
||||
| Déplacement de matière | Quelles opérations participent à cette économie ? Un transport manuel, un convoyeur, un stockage ou une transformation ne sont pas encore déclarés équivalents. |
|
||||
| Expansion | Comment la préparation et la création d’un continent se rattachent-elles aux matériaux engagés et aux espaces laissés derrière ? |
|
||||
|
||||
« Ballast » ne fixe pas encore une quantité, une unité, un coût ou une monnaie.
|
||||
Il faut notamment décider s’il représente une matière effectivement retirée,
|
||||
une trace de son traitement ou les deux selon les opérations. Aucun taux de
|
||||
conversion ni conservation quantitative stricte n’est établi. Le document
|
||||
n’autorise donc pas à prélever des objets dans les inventaires ou à les dupliquer
|
||||
dans des coffres au nom de cette cosmologie.
|
||||
|
||||
## Lire l’histoire économique dans la géologie
|
||||
|
||||
La génération doit dépendre du comportement réel des joueurs. L’auteur donne
|
||||
trois exemples qui fixent la direction sans constituer des recettes de génération :
|
||||
|
||||
| Histoire du serveur | Trace possible dans les Backrooms |
|
||||
| --- | --- |
|
||||
| Le serveur consomme énormément de pierre. | Les premières couches deviennent minérales. |
|
||||
| Une phase d’industrialisation utilise beaucoup de cuivre. | Des zones techniques apparaissent. |
|
||||
| Des tonnes de bois sont sacrifiées. | Un gigantesque Indoor abandonné peut émerger. |
|
||||
|
||||
Des mois après, les joueurs peuvent descendre et reconnaître dans les lieux
|
||||
traversés une période de l’histoire du serveur. Le choix des matériaux doit
|
||||
pouvoir conserver une différence entre une phase minérale, une phase industrielle
|
||||
et d’autres formes de production, au lieu de tout réduire à la dernière palette
|
||||
dominante.
|
||||
|
||||
Cette intention demande une temporalité explicite. Deux histoires se rencontrent :
|
||||
celle des neuf personnages, antérieure à l’arrivée des joueurs, et celle que le
|
||||
serveur écrit pendant la partie. La première fournit les vestiges d’un autre
|
||||
Minecraft ; la seconde peut produire de nouveaux dépôts et de nouvelles traces.
|
||||
La présence de strates initiales issues de l’ancienne partie, leur contenu et
|
||||
leur articulation avec les apports actuels restent à décider.
|
||||
|
||||
Il faut encore choisir la durée des périodes observées, ce qui forme un dépôt,
|
||||
le moment où un dépôt devient un lieu et la manière dont les périodes se
|
||||
succèdent dans l’espace. Une couche n’est pas nécessairement un niveau vertical :
|
||||
la géographie de cette chronologie reste à dessiner. La règle devra préserver
|
||||
les espaces déjà visités et construits ; changer les statistiques actuelles ne
|
||||
doit pas devenir une régénération implicite d’anciens chunks.
|
||||
|
||||
## Ce que le socle méta peut fournir
|
||||
|
||||
Le Blocodex appartient à Sanctuary et peut être consulté par plusieurs mécaniques.
|
||||
Il distingue actuellement connaissance, observation, minage, possession passée,
|
||||
jet, pose et stock porté. Les critères de connaissance et les familles de
|
||||
matériaux peuvent aider un futur moteur à constituer une palette utilisable.
|
||||
|
||||
Ces informations personnelles ne suffisent cependant pas à décrire l’histoire économique :
|
||||
|
||||
- **miné** ne veut pas dire consommé par le shop ou engagé dans une machine ;
|
||||
- **jeté** compte les jets volontaires vanilla, pas les objets brûlés ou perdus
|
||||
dans le vide ;
|
||||
- **déjà possédé** est une preuve persistante, pas un stock encore disponible ;
|
||||
- **posé** ne fournit pas à lui seul le plan d’une construction ni sa date ;
|
||||
- le stock personnel ne recense pas tous les coffres et réseaux du serveur.
|
||||
|
||||
Le socle méta distingue maintenant trois relevés :
|
||||
|
||||
| Relevé | Ce qu’il permet de connaître |
|
||||
| --- | --- |
|
||||
| Mémoire personnelle | Ce qu’un joueur a vu, miné, possédé, jeté ou posé ; des critères pour ses palettes. |
|
||||
| Distribution actuelle | Les blocs du terrain chargé et les stocks accessibles, comptés séparément avec leur couverture. |
|
||||
| Activité datée du serveur | Les quantités minées, posées, ramassées, jetées et fabriquées, agrégées par jour UTC depuis l’installation. |
|
||||
|
||||
Le troisième relevé conserve une trace des périodes d’activité après le
|
||||
déplacement des stocks. Il peut montrer une période de minage intense de cuivre,
|
||||
mais ne prouve pas que ce cuivre a été sacrifié à une machine. Il ne reconstruit
|
||||
pas les mois précédant son installation et ne calcule aucun ballast. Les limites
|
||||
de persistance et de couverture figurent dans le [contrat méta](blocodex.md).
|
||||
|
||||
Avant la génération historique, il faudra donc définir quels faits les systèmes
|
||||
économiques enregistrent : action réalisée, matériau, quantité, moment ou période,
|
||||
origine et destination utiles à cette action. Leur traitement devra distinguer
|
||||
une opération terminée d’une tentative refusée et éviter de compter plusieurs
|
||||
fois la même opération après reprise. La granularité, la conservation et le
|
||||
format de ces transactions restent à concevoir dans un contrat propre. Les
|
||||
agrégats journaliers déjà disponibles ne sont pas un journal transactionnel :
|
||||
ils ne doivent autoriser ni prélèvement, ni récompense, ni restitution d’objet.
|
||||
|
||||
## Machines, références et anciennes solutions
|
||||
|
||||
Les computers pourraient expérimenter dans les Indoors parce qu’ils offrent
|
||||
des espaces contrôlés. Il reste à préciser ce qu’ils demandent à un espace et
|
||||
comment cette demande le rattache à une installation, une porte ou un usage.
|
||||
La cause d’un orphelin dans la fiction devra être compréhensible sans demander
|
||||
au joueur de connaître les fichiers de sauvegarde.
|
||||
|
||||
L’assembleur, le contrôleur, le terminal, le dépôt et l’ancre spatiale restent
|
||||
des concepts en discussion dans [le cahier des machines](structures-conception.md#ordinateurs-et-installation-dexpansion).
|
||||
Les questions communes sont maintenant plus précises : qui formule la demande
|
||||
d’espace, qui fournit sa matière, qui conserve sa référence, qui présente son
|
||||
état, et comment une fermeture ou une interruption est représentée ? Cette liste
|
||||
ne répartit pas arbitrairement cinq fonctions entre cinq blocs.
|
||||
|
||||
Les ruines peuvent rendre ces questions visibles par leurs usages passés.
|
||||
L’observatoire, la salle d’expansion rééquipable, l’atelier caché et la grande
|
||||
traversée restent la sélection de conception actuelle. Pour chacun, il faut
|
||||
retrouver ce qui y était observé, préparé, fabriqué ou relié, puis montrer les
|
||||
extensions et le travail interrompu. Ce sont des questions de fiche de lieu,
|
||||
pas la confirmation de machines présentes ni d’un accès aux Backrooms dans
|
||||
chaque ruine. Le donjon majeur conserve son boss et son objet de quête à définir ;
|
||||
il n’est pas automatiquement identifié au réacteur ou à l’origine de la panne.
|
||||
|
||||
La même ligne relie le passé et la partie actuelle :
|
||||
|
||||
> une infrastructure dont l’auteur a progressivement disparu derrière son
|
||||
> fonctionnement.
|
||||
|
||||
## Accès, retour et récupération : règles encore ouvertes
|
||||
|
||||
La vision antérieure envisage le lit pour entrer dans les Backrooms, des sources
|
||||
de lumière pour les explorer et des coffres recueillant des objets perdus dans
|
||||
le vide ou brûlés. Elle prévoit aussi des accès d’Indoors liés à la mailbox de
|
||||
leur propriétaire. Ces intentions doivent être raccordées à la cosmologie du
|
||||
ballast ; elles ne définissent pas encore un parcours complet.
|
||||
|
||||
Avant un ticket jouable, il faut notamment arrêter :
|
||||
|
||||
1. Les moyens d’entrer, les destinations d’arrivée, le retour et le comportement
|
||||
lors d’une mort ou d’une déconnexion.
|
||||
2. La propriété et le partage des Indoors, la perte de leur objet d’accès, leur
|
||||
fermeture et la distinction entre un accès absent et un espace fictionnellement
|
||||
devenu orphelin.
|
||||
3. Le rapport entre ballast, géologie et objets récupérables : quelle part fait
|
||||
décor ou ressource, quelle part demeure un objet identifiable, et comment sa
|
||||
récupération évite une restitution multiple.
|
||||
4. Les opérations qui alimentent réellement les dépôts, leurs unités, leur
|
||||
cadence et les limites de stockage et de génération.
|
||||
5. Les indices permettant de lire une période économique ou une ancienne solution,
|
||||
sans attribuer d’avance une cause à tous les lieux et aux neuf disparus.
|
||||
|
||||
Les pool rooms et salles étranges physiques sous l’île restent des structures
|
||||
de l’Overworld. Leur ressemblance éventuelle avec les Backrooms ne les transforme
|
||||
pas automatiquement en espaces orphelins. Les passages entre ces lieux, les
|
||||
Indoors et les Backrooms devront être décidés explicitement.
|
||||
|
||||
La prochaine étape de conception peut prendre un seul cas complet : une
|
||||
opération économique réelle, la trace qu’elle conserve, son dépôt, puis le lieu
|
||||
que ce dépôt permettrait de générer et d’explorer. Ce cas permettrait de fixer un
|
||||
premier contrat vérifiable sans prétendre que toute la cosmologie est déjà jouable.
|
||||
@@ -1,296 +0,0 @@
|
||||
# Climats, expansion et ressources
|
||||
|
||||
Ce document distingue les outils de génération et la progression collective
|
||||
encore à construire. L’alpha.13 réunit l’île de départ et
|
||||
les commandes d’expansion dans le choix public **Sanctuary**. Le
|
||||
[guide alpha.13](alpha13-worldgen.md) décrit leur utilisation. Les versions
|
||||
alpha.12/12.1 et leur [laboratoire](alpha12-laboratory.md) restent historiques.
|
||||
Le déblocage par recherches, XP et contributions n’est pas encore implémenté.
|
||||
|
||||
## Taille initiale et extensions — alpha.13
|
||||
|
||||
Le bouton natif **Personnaliser** propose **Petit** (512 blocs de diamètre
|
||||
nominal), **Moyen** (724, par défaut) et **Grand** (1 024). La hauteur reste de
|
||||
384 blocs. Ce choix configure un nouveau monde ; il ne promet ni nombre de
|
||||
joueurs, ni quantité de ressources, ni aire entièrement constructible.
|
||||
|
||||
L’île initiale reprend le relief, les forêts et l’hydrologie de Population,
|
||||
avec les poches de cavités natives issues du laboratoire. Les îles ajoutées
|
||||
conservent les tailles **64/128/256/512**, les douze profils climatiques et les
|
||||
quatre reliefs d’expansion. Leur génération ne reprend pas automatiquement
|
||||
la grande rivière et les terrasses de l’île initiale.
|
||||
|
||||
Les commandes opérateur `quick`, `create`, `preview`, `list`, `visit` et
|
||||
`resume` reprennent le fonctionnement alpha.12.1. Le paramètre final de distance
|
||||
est facultatif ; les sites occupés sont écartés en cherchant plus loin dans la
|
||||
même direction. Même un chunk de vide déjà généré est protégé. Les recherches
|
||||
collectives et les objets de déblocage ne sont pas nécessaires à ces outils.
|
||||
|
||||
La racine alpha.13 est de génération 13 ; les extensions restent de génération
|
||||
12, dans le journal `data/sanctuary-world-v13/expansions.json` du nouveau monde.
|
||||
Les anciens mondes gardent leurs codecs, leurs paramètres et leur journal.
|
||||
Les choix séparés Petit/Grand et Laboratoire sont désormais cachés à la
|
||||
création. Aucun monde existant n’est converti ou redimensionné.
|
||||
|
||||
## Tailles et mesures historiques
|
||||
|
||||
L’alpha.11 proposait Petit (512) et Grand (724), avec Grand par défaut ; ses
|
||||
repères internes 5/10 ne correspondaient pas à une capacité multijoueur promise.
|
||||
Les références `population_5/10/20/100` restent celles des sauvegardes historiques.
|
||||
Les mesures alpha.3 à alpha.11 conservées ci-dessous décrivent uniquement les
|
||||
versions et les emprises observées à leur date. Les contrôles de l’alpha.13,
|
||||
les rechargements et le benchmark Moyen sont consignés dans [Validation](testing.md).
|
||||
|
||||
## Les huit directions et les branches climatiques
|
||||
|
||||
Sanctuary Island constitue le point de départ **tempéré, légèrement humide**.
|
||||
La boussole climatique est une convention facultative : nord froid, sud chaud,
|
||||
ouest sec et est humide. Les diagonales combinent ces tendances. **La direction
|
||||
géographique ne contraint plus le climat.** Les joueurs pourront choisir de
|
||||
prolonger une voie climatique ou de bifurquer vers d’autres découvertes.
|
||||
|
||||
| Direction | Axe Minecraft depuis Sanctuary | Tendance climatique |
|
||||
| --- | --- | --- |
|
||||
| Nord | `−Z` | Froid |
|
||||
| Nord-est | `+X, −Z` | Froid et humide |
|
||||
| Est | `+X` | Humide |
|
||||
| Sud-est | `+X, +Z` | Chaud et humide |
|
||||
| Sud | `+Z` | Chaud |
|
||||
| Sud-ouest | `−X, +Z` | Chaud et sec |
|
||||
| Ouest | `−X` | Sec |
|
||||
| Nord-ouest | `−X, −Z` | Froid et sec |
|
||||
|
||||
Ces associations sont les raccourcis par défaut, pas une loi du monde.
|
||||
Une branche conserve son parent, sa direction et son climat explicite. Plusieurs
|
||||
couronnes peuvent donc proposer des biomes nouveaux au nord comme au sud.
|
||||
Le point d’origine reste enregistré même si un joueur déplace son lit ou le spawn.
|
||||
Les familles de climats ne sont pas des biomes uniformes.
|
||||
Le relief, l'altitude, les variations locales et la graine doivent conserver
|
||||
des géographies distinctes à l'intérieur de chaque continent. Les amplitudes
|
||||
de température et d'humidité, leur évolution avec la distance et les seuils
|
||||
de sélection des biomes feront partie du ticket de génération des continents.
|
||||
Les ressources et cultures futures, notamment celles d'It's Alive !, pourront
|
||||
ainsi demander l'exploration de plusieurs climats.
|
||||
|
||||
L’alpha.5 proposait cinq biomes tempérés dont Dappled Forest. L’alpha.6 réserve
|
||||
ce dernier aux futurs continents et donne à l’île une palette plus douce de
|
||||
prairies, prairies fleuries, bosquets, corniches moussues et zones rocheuses.
|
||||
La répartition tient compte de l’altitude, y compris dans les strates inférieures.
|
||||
La variation locale et l’humidité dépendent de la graine ; elles ne
|
||||
constituent pas encore le champ de recherche directionnel des continents.
|
||||
|
||||
L’alpha.7 densifie les forêts de chênes et de bouleaux en surface et donne
|
||||
aux corniches intérieures humides des chênes noirs et des champignons.
|
||||
Des champignons vanilla peuvent aussi se rencontrer en surface. Les
|
||||
clairières fleuries et les affleurements restent présents. Les essences rares
|
||||
et un éventuel arbre remarquable dépendent du terrain ; chaque graine n’est
|
||||
pas tenue de fournir tous les bois. Les minerais de départ peuvent affleurer,
|
||||
avec des petits filons et sans correction des stocks après comptage.
|
||||
|
||||
## Eau de surface et exploration
|
||||
|
||||
L'île de départ doit pouvoir accueillir naturellement des étangs et lacs de
|
||||
surface, des berges propices à la canne à sucre et, lorsque le relief s'y prête,
|
||||
des rivières. L'eau s'inscrit dans les creux et les chemins du terrain existant.
|
||||
Les bassins gardent un fond et des berges fermés. Une grande rivière reste
|
||||
facultative : elle doit suivre le relief et former un parcours lisible avec
|
||||
source, chute et bassin. Ses exutoires peuvent laisser couler l’eau dans le vide.
|
||||
Sans parcours suffisamment long, l’île conserve seulement ses petits bassins.
|
||||
L’alpha.6 livre cette variante à bassins seuls ; la grande rivière reste à développer.
|
||||
L’alpha.5 ajoute séparément quelques sources sortant d’une niche rocheuse
|
||||
naturelle : leurs cascades peuvent descendre dans le vide et servir à remonter
|
||||
verticalement. Cet écoulement est volontaire ; il ne doit pas entraîner des
|
||||
fuites sur toutes les rives.
|
||||
|
||||
Le contrat ne demande ni un bassin souterrain imposé pour obtenir un stock
|
||||
d'eau, ni une cuvette ajoutée systématiquement à une position fixe. Une surface
|
||||
d'eau doit reposer sur un fond naturel et disposer de berges cohérentes ; un
|
||||
volume rocheux fermé sous terre ne devient pas un lac de surface. Les plantations
|
||||
de canne à sucre doivent respecter leurs conditions de placement, sans modifier
|
||||
la carte pour atteindre une quantité prédéterminée.
|
||||
|
||||
Un étang, un lac, une rivière et un bassin souterrain sont donc des résultats
|
||||
différents à vérifier. Un prototype d'étangs ne valide pas automatiquement les
|
||||
rivières ou l'ensemble de l'hydrologie. Le détail effectivement livré à chaque
|
||||
version reste décrit dans [Génération](worldgen.md), avec ses limites.
|
||||
|
||||
## Observer les ressources après génération
|
||||
|
||||
La génération produit le monde ; l'économie apprend ensuite ce que le monde
|
||||
contient. On ne force pas un nombre de blocs de pierre, de bûches ou de minerais
|
||||
pour faire correspondre la géographie à un budget. Un relevé peut constater
|
||||
qu'une ressource est absente, rare ou abondante pour une graine donnée. Ce constat
|
||||
ne déclenche pas de remplissage ni de remplacement de blocs.
|
||||
|
||||
Le relevé initial servira de référence pour préparer l'économie et la progression
|
||||
collective. Il distingue les blocs présents, les ressources réellement accessibles
|
||||
aux joueurs, les ressources renouvelables et les objets obtenus en les récoltant.
|
||||
Un bloc de minerai n'est pas un nombre garanti d'objets : outils, enchantements,
|
||||
butins et transformations relèvent d'autres règles. Les blocs de bois ne comptent
|
||||
pas des arbres, et un comptage de blocs d'eau ne mesure pas un débit de rivière.
|
||||
|
||||
Une observation dans une petite zone reste une observation locale. On ne peut
|
||||
pas multiplier son résultat pour annoncer la quantité totale de l'île. Les
|
||||
rapports de continents devront conserver leur propre graine, version, emprise
|
||||
et état de génération, afin d'additionner seulement des régions connues et
|
||||
disjointes lorsque le besoin économique sera défini.
|
||||
|
||||
## Ressources de départ — depuis l’alpha.5
|
||||
|
||||
L’île vise les premières étapes de survie, les outils, l’enchantement et la
|
||||
redstone. Les sept minerais (charbon, fer, cuivre, or, redstone, lapis et diamant)
|
||||
utilisent des petites veines à des altitudes compatibles avec le terrain relevé.
|
||||
Le charbon et le fer ne reprennent plus leurs fréquences vanilla beaucoup trop
|
||||
généreuses dans cette géométrie. Une petite réserve de lave peut apparaître dans une niche rocheuse accessible
|
||||
pour les premières productions et l’accès au Nether. Le bois, la pierre, les animaux et les cultures
|
||||
renouvelables conservent leur fonctionnement Minecraft : ce réglage des minerais
|
||||
ne constitue pas une interdiction des grandes constructions.
|
||||
|
||||
Les placements restent probabilistes, sans quota de rattrapage après comptage.
|
||||
Une graine peut donc manquer d’une ressource ; la présence de tout le parcours
|
||||
Minecraft n’est pas garantie. Les structures donnant accès à l’End et leur lien
|
||||
avec l’expansion restent à concevoir.
|
||||
|
||||
## Relevé de développement — générateurs historiques
|
||||
|
||||
`ResourceSurveyDiagnostics`, dans `src/gametest`, lit les chunks au statut
|
||||
`FULL`, après le terrain, les matériaux et les décorations. Il ne figure pas
|
||||
dans le JAR Sanctuary distribué et n'ajoute aucune commande joueur ou admin.
|
||||
La tâche repart d'un monde GameTest neuf, jamais d'une sauvegarde personnelle.
|
||||
Depuis l’alpha.13, `runGameTest` et `check` sélectionnent la suite unifiée par
|
||||
défaut. Les commandes ci-dessous demandent explicitement une suite historique ;
|
||||
elles ne constituent pas un relevé du nouveau monde unifié.
|
||||
|
||||
Le relevé standard inspecte **neuf chunks**, soit un carré de 48×48 blocs autour
|
||||
du chunk du spawn naturel, sur toute la hauteur de la dimension. Il prépare
|
||||
d'abord ces chunks et un halo voisin, puis compte exactement chaque cellule des
|
||||
palettes de leurs sections. La grille éloignée du framework est exclue et sa
|
||||
distance est vérifiée. Aucun objet posé, inventaire, contenu de coffre ou mob
|
||||
n'est inclus dans ces comptes de blocs.
|
||||
|
||||
```sh
|
||||
# Contrôle standard : neuf chunks inspectés, total local uniquement.
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryUnifiedTests=false -PsanctuaryResourceSurvey=sample --console=plain
|
||||
|
||||
# Relevé de toute l'enveloppe déclarée de Sanctuary, sur un monde jetable neuf.
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryUnifiedTests=false -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=island --console=plain
|
||||
|
||||
# Désactiver seulement le relevé pour un autre diagnostic.
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryUnifiedTests=false -PsanctuaryResourceSurvey=off --console=plain
|
||||
```
|
||||
|
||||
Les modes sont transmis à la propriété JVM de test
|
||||
`sanctuary.test.resourceSurvey`. La propriété Gradle `-PsanctuaryTestPlayers`
|
||||
choisit le profil interne. L’alpha.11 utilise `5` pour Petit et `10` pour Grand,
|
||||
ce dernier par défaut ; `20` et `100` servent à la compatibilité historique.
|
||||
Le mode `island` calcule l’enveloppe du profil actif, avec 16 blocs de marge de
|
||||
décoration et un halo de préparation d’un chunk. Les valeurs ci-dessous sont
|
||||
les bornes calculées lors de l’alpha.10, pas des relevés exécutés ni un nombre
|
||||
de chunks déjà validé pour l’ancien Grand alpha.11 :
|
||||
|
||||
| Profil | Chunks comptés par `island` | Chunks FULL préparés avec halo |
|
||||
| --- | --- | --- |
|
||||
| 5 | 1 444 | 1 600 |
|
||||
| 20 | 4 900 | 5 184 |
|
||||
| 100 | 22 500 | 23 104 |
|
||||
|
||||
Un relevé de toute l’emprise peut être très coûteux et ne s’exécute pas par
|
||||
défaut. La marge devra être revue si une future génération produit des
|
||||
structures ou décorations qui la dépassent. Les diagnostics alpha.10 courants
|
||||
restent des inspections ciblées ; leurs nombres de minerais ne sont pas ceux
|
||||
d’un relevé `island` complet.
|
||||
|
||||
Les nouveaux fichiers sont nommés
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/resource-survey-players-<capacité>-seed-<graine>-<mode>.json`.
|
||||
Sur un générateur historique, le nom émis par l’outil actuel porte `legacy`.
|
||||
Les anciens chemins `resource-survey-seed-…` cités plus bas désignent uniquement
|
||||
les relevés archivés de leurs versions respectives.
|
||||
|
||||
Il contient :
|
||||
|
||||
- La graine, la version Minecraft, le preset et les réglages de génération,
|
||||
ainsi que `generator.version = sanctuary@<version du mod>`.
|
||||
- Le spawn naturel, les bornes exactes en blocs et chunks, les hauteurs de la
|
||||
dimension et du bruit, les coordonnées de chaque chunk inspecté au statut
|
||||
`FULL`, les dates UTC et les durées de préparation et de comptage.
|
||||
- Le total de toutes les cellules, les cellules d'air, les blocs non vides,
|
||||
puis les comptes par identifiant de registre et par état complet, propriétés
|
||||
comprises, par exemple `minecraft:oak_log[axis=y]`.
|
||||
- Des agrégats explicites pour la pierre, les bûches, les feuilles, les minerais
|
||||
de charbon, fer, cuivre, or, redstone, lapis, diamant et émeraude, l'eau, la
|
||||
lave, le sable, le gravier, l'argile, la canne à sucre et les sols.
|
||||
- La complétude dans les bornes demandées, la couverture ou non de l'enveloppe
|
||||
déclarée de l'île et l'absence d'extrapolation. Aucun mode ne prétend inventorier
|
||||
tout le monde Minecraft, ses autres dimensions ou ses futures expansions.
|
||||
|
||||
Les variantes ordinaires et deepslate des minerais sont détaillées. Les agrégats
|
||||
fondés sur des tags indiquent leur définition et les membres observés. Certains
|
||||
groupes se recouvrent, comme `stone` et `base_stone_overworld`, ou les blocs
|
||||
d'eau et les blocs contenant un état d'eau : ils ne doivent pas être additionnés.
|
||||
Les sommes des identifiants et des états doivent chacune être exactement égales
|
||||
au volume inspecté, y compris l'air ; une incohérence fait échouer le GameTest.
|
||||
|
||||
Ce rapport est un instantané après génération, avec l'heure du jeu observée.
|
||||
Il ne prédit pas les pousses, les récoltes, les pertes ni la production future.
|
||||
Le format JSON est versionné pour l'outil de test ; ce n'est pas encore un
|
||||
format de sauvegarde serveur ou un registre persistant de l'économie.
|
||||
|
||||
## Référence mesurée — graine 0, alpha.3
|
||||
|
||||
Le **8 septembre 2026**, un relevé complet de l'enveloppe déclarée a été effectué
|
||||
sur un monde neuf de graine **0**, avec `sanctuary@0.1.0-alpha.3` et les réglages
|
||||
`sanctuary:sanctuary_384`. Il couvre **1 444 chunks FULL**, de **−304 inclus à
|
||||
304 exclu en X et Z**, et de **0 inclus à 384 exclu en Y**. Le volume inspecté
|
||||
est donc exactement **608 × 608 × 384 = 141 950 976 cellules**.
|
||||
|
||||
| Ressource observée | Nombre de blocs |
|
||||
| --- | ---: |
|
||||
| Pierre (`minecraft:stone`) | 7 355 184 |
|
||||
| Bûches, tag `minecraft:logs` | 10 946 |
|
||||
| Minerai de charbon | 101 399 |
|
||||
| Minerai de fer | 67 198 |
|
||||
| Minerai de cuivre | 2 352 |
|
||||
| Minerai de diamant | 6 |
|
||||
| Minerais d'or, redstone, lapis et émeraude | 0 chacun |
|
||||
| Gravier | 121 044 |
|
||||
| Terre et blocs d'herbe | 728 630 |
|
||||
| Feuilles | 117 735 |
|
||||
| Eau, y compris les blocs contenant de l'eau | 0 |
|
||||
| Canne à sucre | 0 |
|
||||
| Sable, argile et lave | 0 chacun |
|
||||
|
||||
Les bûches comprennent **8 765 bûches de chêne** et **2 181 de bouleau**.
|
||||
Les minerais présents sont ici leurs variantes ordinaires ; aucune variante
|
||||
deepslate n'a été observée. Toutes catégories confondues, le relevé compte
|
||||
**8 584 514 blocs non vides** et **133 366 462 cellules d'air**. Ce sont les
|
||||
quantités observées dans cette graine et cette version, sans quota ni correction
|
||||
du terrain. Les zéros d'eau et de canne à sucre confirment que l'hydrologie
|
||||
demandée reste à réaliser ; ils ne constituent pas une cible à conserver.
|
||||
|
||||
La source détaillée est le fichier local non versionné
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/resource-survey-seed-0-island.json`,
|
||||
terminé à **11:34:08 UTC**. Une lecture indépendante du JSON a confirmé les
|
||||
1 444 coordonnées de chunks distinctes, les sommes par chunk, les agrégats et
|
||||
l'égalité exacte entre le volume, le compte par identifiant et le compte par
|
||||
état complet. Les huit tests requis du serveur ont réussi pendant cette exécution.
|
||||
|
||||
La préparation des chunks a pris **782 659 ms**, soit **13 min 2,659 s** ; le
|
||||
comptage et la préparation des données à sérialiser ont pris **135 ms**. Cette
|
||||
mesure locale a été effectuée sur un **Apple M1 avec 8 Go de mémoire**, avec un
|
||||
essai client concurrent pendant environ 23 secondes. Elle décrit une machine
|
||||
chargée et cette exécution, pas une performance garantie sur d'autres machines.
|
||||
Le temps de préparation est distinct du coût du comptage par palettes.
|
||||
|
||||
## Contrat de progression collective restant à construire
|
||||
|
||||
Le registre et la réservation des alpha.12/12.1 sont repris dans l’alpha.13 :
|
||||
identifiant, parent, direction, centre, emprise, graine, génération, climat et
|
||||
statut sont enregistrés côté serveur. Les chunks existants, y compris le vide,
|
||||
sont protégés pendant la recherche d’une nouvelle région.
|
||||
|
||||
Le prochain travail de progression doit relier ces ouvertures aux recherches,
|
||||
aux contributions et à leurs coûts. Un éventuel relevé de ressources intervient
|
||||
après la génération réelle, avec une complétude explicite ; il ne choisit pas
|
||||
une graine pour remplir un stock attendu. Les prix, les objets de déblocage et
|
||||
les règles de contribution restent à concevoir. Aucun contrat de migration
|
||||
ou de régénération d’anciennes parties n’est fourni par ce diagnostic.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Alpha.23.1 — île naturelle, secrets et continents
|
||||
|
||||
Contrat du ticket **WG-27**, branche `codex/natural-secrets-alpha23`.
|
||||
Cette livraison réunit le Blocodex alpha.23 et la génération `sanctuary:island_v23`.
|
||||
Les résultats des essais sont consignés dans [le rapport](testing-alpha23.1.md).
|
||||
|
||||
## Sanctuary Island
|
||||
|
||||
Le point de départ reste naturel. Les quartiers Lost City et les infrastructures
|
||||
ordinaires Sanctuary ne sont plus demandés sur l’île initiale. Leur code et les
|
||||
anciennes générations sont conservés ; les expansions Lost City restent possibles.
|
||||
Les structures vanilla ne sont pas supprimées : leur admission dépend du biome,
|
||||
du relief, des appuis et des volumes déjà occupés.
|
||||
|
||||
La sélection des lieux Sanctuary conserve l’observatoire, un atelier caché,
|
||||
la grande traversée et la salle d’expansion indépendante. La piscine attenante
|
||||
est retirée ; les égouts et leurs ouvertures restent possibles. Les anciennes voies
|
||||
ferrées peuvent exister sans gare ni bâtiment au bout : leur remise en service
|
||||
reste une construction des joueurs.
|
||||
|
||||
Le **sanctuaire minéral** est une grande cavité géologique explorable, dominante
|
||||
pierre et cobblestone, avec voûtes, piliers, corniches et filons minables. Il peut
|
||||
affleurer sur les flancs ou sous l’île avec des attaches rocheuses. L’enveloppe
|
||||
maximale est 79 × 73 × 27 ; ce volume ne désigne pas une façade rectangulaire.
|
||||
Il ne comporte ni briques, mobilier, machines, ni éclairage manufacturé.
|
||||
Le mot laboratoire reste réservé à la future structure des Mooblooms.
|
||||
|
||||
Le gardien et le portail des Cavernes ne sont pas activés ici. L’intention de
|
||||
progression demeure : vaincre le gardien stabilisera l’accès aux Cavernes pour
|
||||
tout le serveur, puis chacun pourra construire son portail. Le Deep Dark et les
|
||||
cités anciennes appartiendront à ces Cavernes. Le lieu des raids appartient aux
|
||||
expansions et n’est pas construit dans cette livraison.
|
||||
|
||||
## Structures natives et garantie d’expansion
|
||||
|
||||
Le catalogue s’appuie sur les 52 identifiants de structures présents dans le JAR
|
||||
Minecraft 26.3-pre-2 : 47 de l’Overworld, dont la cité ancienne réservée aux Cavernes,
|
||||
et cinq propres au Nether ou à l’End. Les petits donjons `monster_room` sont des
|
||||
features et font l’objet d’un suivi séparé. Les modèles, identifiants, coffres,
|
||||
spawners et comportements natifs sont conservés.
|
||||
|
||||
Les placements distinguent surface, sous-sol et milieu aquatique. Les hauteurs
|
||||
souterraines vanilla ne sont pas recopiées sous Y=0 : elles sont adaptées à la
|
||||
roche de l’île. Les monuments conservent aussi leur hauteur lors du rechargement.
|
||||
L’adaptation ne remplace pas les définitions `minecraft:*` du registre.
|
||||
Les ruines océaniques et les trésors enterrés conservent leur ajustement fin
|
||||
vanilla au fond réel pendant la pose des blocs : leur hauteur peut encore se
|
||||
préciser après l’acceptation du départ.
|
||||
|
||||
**Correction approuvée après le plan initial : chaque nouvelle expansion 23 avec
|
||||
les structures activées doit disposer d’au moins une structure adaptée avant sa
|
||||
réservation.** Les familles absentes restent préférées, sans rendre obligatoire
|
||||
une famille incompatible. Une recherche infructueuse ne publie pas une île vide
|
||||
de structures et ne contourne pas les exigences d’appui. Aucune structure n’est
|
||||
ajoutée rétroactivement aux îles déjà enregistrées.
|
||||
|
||||
Les yeux de l’Ender, cartes d’exploration et `/locate` recherchent les vrais
|
||||
départs dans les îles ouvertes. Une recherche n’active jamais d’expansion.
|
||||
Le compteur natif de références cartographiques n’indique pas une visite réelle.
|
||||
|
||||
## Océans
|
||||
|
||||
Le relief `ocean` est réservé aux expansions 23 de 512 ou 1024 blocs. Les grandes
|
||||
expansions rapides sans relief explicite peuvent le choisir dans un tirage
|
||||
déterministe d’environ un tiers. Le climat et la taille restent ceux du descripteur.
|
||||
|
||||
Le champ océanique commun au terrain, à l’eau et aux biomes produit une mer
|
||||
intérieure irrégulière, des hauts-fonds, des côtes et une zone profonde, en
|
||||
conservant une majorité de terres. Cible : 30–45 % de surface aquatique nominale,
|
||||
profondeurs maximales 32–56 blocs. Une coque rocheuse continue retient l’eau ;
|
||||
le reste du vide conserve l’air. Le niveau d’eau est local à l’expansion.
|
||||
Ce niveau accompagne aussi les décorations natives, notamment les icebergs ;
|
||||
il est rétabli après chaque passe pour ne pas affecter les autres îles.
|
||||
|
||||
Les températures utilisent les biomes natifs océan, océan froid, gelé, tiède et
|
||||
chaud, avec les variantes profondes disponibles. Il n’existe pas de
|
||||
`deep_warm_ocean` natif. Les monuments exigent un océan profond réellement adapté.
|
||||
Aucun océan ni Deep Dark n’est ajouté à Sanctuary Island.
|
||||
|
||||
## Mémoire serveur et sauvegardes
|
||||
|
||||
Le registre des structures est persistant et distinct du census des seuls chunks
|
||||
chargés. Il distingue départ accepté, bâtiment partiellement ou complètement
|
||||
généré, région encore inconnue et absence confirmée dans une région vérifiée.
|
||||
Une démolition ne supprime pas le fait historique de génération. Une erreur de
|
||||
lecture ne transforme pas le registre en liste vide.
|
||||
|
||||
`/sanctuary census structures` expose les relevés aux opérateurs ; l’option
|
||||
`island <id>` précise la région et `dimension <id>` distingue les dimensions natives.
|
||||
L’île propriétaire se déduit des descripteurs immuables du journal d’expansion.
|
||||
Le suivi automatique ne charge pas la carte et
|
||||
ne révèle pas les secrets dans le Blocodex personnel.
|
||||
|
||||
Le champ optionnel plat `structure_priorities` d’une nouvelle expansion 23
|
||||
contient les familles bonifiées. Il est capturé une fois avant l’inspection,
|
||||
figé dans le journal et lié au certificat de réutilisation du vide. Bonus initial
|
||||
de sélection : × 4. Une reprise réutilise ce profil même si le registre a évolué.
|
||||
Les générations historiques restent neutres, sans champ ajouté à leurs écritures.
|
||||
|
||||
Les mondes existants gardent leur codec et leur terrain. Les nouvelles règles
|
||||
nécessitent un nouveau monde ; aucune conversion ou régénération n’est effectuée.
|
||||
Petit 512, Moyen 724 par défaut et Grand 1024 restent dans Personnaliser. L’option
|
||||
expérimentale commande les secrets optionnels et les rails ; le hall et la
|
||||
traversée restent dans le socle. Les structures vanilla en sont indépendantes.
|
||||
La désactivation générale des structures reste prioritaire.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Alpha.24 — les lieux suspendus de Sanctuary
|
||||
|
||||
## Contrat
|
||||
|
||||
Le nouveau codec `sanctuary:island_v24` ajoute cinq lieux au monde initial :
|
||||
une montgolfière, deux bateaux de fret et deux îlots minéraux. Ils sont planifiés
|
||||
à la création et construits seulement lorsque leurs chunks sont générés.
|
||||
La génération de terrain reste celle de l’alpha.23.1 : mêmes trois tailles
|
||||
512 / 724 / 1 024, Moyen par défaut, bouton Minecraft **Personnaliser**,
|
||||
secrets minéraux, voies abandonnées, structures vanilla, océans d’expansion,
|
||||
registre et admission d’au moins une structure adaptée par expansion acceptée.
|
||||
|
||||
Les cinq sites cherchent la périphérie, hors de l’emprise du terrain initial.
|
||||
Chaque site possède un point de rivage vérifié, à moins de 512 blocs, et un
|
||||
segment de vue dégagé dans le relief et hors des volumes des secrets déjà
|
||||
planifiés. Les distances de rendu du client et du
|
||||
serveur déterminent sa visibilité en jeu ; les arbres ultérieurs peuvent
|
||||
masquer une partie de cette vue. Le relief n’est pas prolongé vers les sites.
|
||||
Aucun pont, quai, pilier jusqu’au sol ou accès automatique n’est fourni.
|
||||
|
||||
## Formes et ressources
|
||||
|
||||
La montgolfière possède une enveloppe arrondie de laine claire, un accent,
|
||||
une nacelle en chêne et des suspentes attachées. Elle reste sous Y350.
|
||||
Les deux navires ont une coque biseautée, des voiles, un pont protégé,
|
||||
une cabine et une cale. Le navire de vivres accueille un fermier adulte ;
|
||||
celui de matériel, un outilleur adulte. Leurs postes de travail sont vanilla.
|
||||
Il faut construire pour atteindre les lieux et ramener les villageois.
|
||||
|
||||
Les îlots ont une roche irrégulière, une base effilée et des filons connectés.
|
||||
Leur surface reçoit une couverture de terre et d’herbe majoritaire, des fleurs,
|
||||
des herbes et quelques petits chênes ou bouleaux. Le volume réservé comprend
|
||||
les couronnes ; l’épaisseur rocheuse reste de 20 à 35 blocs.
|
||||
Leur profondeur relative détermine les minerais : charbon, fer et cuivre
|
||||
abondants ; poches d’or, lapis, redstone, diamant et émeraude. Ces réserves
|
||||
sont finies. Le recensement existant compte les blocs dans les chunks
|
||||
observés, sans charger toute la région ni lire le butin encore fermé.
|
||||
Exactement un îlot possède une source d’eau qui s’écoule suivant les règles
|
||||
Minecraft et peut tomber dans le vide.
|
||||
|
||||
Chaque lieu contient exactement un coffre dédié. Ceux des îlots sont enterrés
|
||||
sous une couverture stable, à l’écart de la cascade. Les tables de butin
|
||||
utilisent exclusivement des objets vanilla, sans récompense future ni Anomaly.
|
||||
Ni le butin, ni les habitants, ni les blocs détruits ne se renouvellent au
|
||||
rechargement d’un chunk déjà généré.
|
||||
|
||||
## Persistance et expansions
|
||||
|
||||
La recherche est déterministe, limitée à 64 candidats par site et 8 192
|
||||
colonnes de terrain mises en cache pour l’ensemble du plan. Les plans des
|
||||
secrets racine sont calculés auparavant, puis réutilisés au démarrage ; une
|
||||
voûte ou une galerie ne peut ainsi recouvrir le point côtier certifié. Cette
|
||||
planification ne demande aucun chunk. Un échec interrompt explicitement la création du plan plutôt que
|
||||
de livrer un monde avec un satellite silencieusement manquant.
|
||||
|
||||
Le manifeste immuable est écrit dans `data/sanctuary-aerial-v24/` avant le
|
||||
premier journal `data/sanctuary-world-v24/`. Son SHA-256 est lié au journal24.
|
||||
Un rechargement relit le plan stocké ; il ne le recalcule pas. Un manifeste
|
||||
manquant, altéré ou incompatible bloque la reprise avant toute expansion.
|
||||
Ce contrat ne s’applique qu’aux nouveaux mondes24 ; les anciens journaux,
|
||||
codecs, profils et terrains restent lisibles sans migration.
|
||||
|
||||
Les réservations couvrent exactement les chunks des cinq structures et un
|
||||
petit masque local autour de la sortie d’eau, sans marge sur tout le pourtour
|
||||
des sites. Elles ne deviennent pas des propriétaires de terrain :
|
||||
les biomes du vide et le bruit de génération restent inchangés. L’admission
|
||||
d’expansion, le journal et la reprise de vide vérifient ces emprises même si
|
||||
aucun joueur n’a encore exploré le site. Les passages entre elles restent libres.
|
||||
|
||||
Les départs et pièces sont natifs, enregistrés sous `aerial_balloon`,
|
||||
`aerial_merchant_ship` et `aerial_mineral_islet`. Les écritures sont limitées
|
||||
au site et au chunk courant. Le registre distingue les sites planifiés,
|
||||
les départs acceptés et la génération observée. Les scopes aériens ne servent
|
||||
pas à conclure qu’une famille de structures vanilla manque au serveur.
|
||||
`/locate structure` et `/sanctuary census structures aerial [site]` les
|
||||
référencent sans ouvrir d’expansion ni transformer une consultation en visite.
|
||||
|
||||
Les options **Structures expérimentales** et **Générer les structures**
|
||||
contrôlent les satellites. Leur désactivation produit un plan vide, sans
|
||||
réservations aériennes. Les options sont sauvegardées avec le monde.
|
||||
|
||||
## Limites de cette livraison
|
||||
|
||||
Minecraft reste en 26.3-pre-2. Les nuages et la hauteur du monde sont inchangés.
|
||||
Aucun véhicule pilotable, bateau-poule, temple, commerce Sanctuary, boss,
|
||||
portail des Cavernes ou raid n’est ajouté. Deep Dark et cités anciennes
|
||||
restent réservés aux futures Cavernes.
|
||||
|
||||
Les vérifications réellement exécutées, les témoins et l’état de distribution
|
||||
figurent dans [testing-alpha24.md](testing-alpha24.md).
|
||||
@@ -1,64 +0,0 @@
|
||||
# Alpha.27 — reprendre la 24 et regarder le résultat
|
||||
|
||||
Le retour du 12 septembre 2026 rejette l'alpha.25 : cônes creux, enveloppes
|
||||
rocheuses trop minces, cavités génériques et île trop perforée. Les tests
|
||||
fonctionnels passaient mais ne démontraient pas un rendu naturel. La branche
|
||||
`codex/reprise-sanctuary-alpha27` repart directement de `e7d1f29`, la base 24
|
||||
publiée ; elle ne désactive pas simplement quelques options de la 25.
|
||||
|
||||
## Cette première passe
|
||||
|
||||
- Retrouver le relief, les cavités et l'hydrologie de la 24. Aucun ajout de
|
||||
montagne, volume de cavité, lac ou plateforme pour faire tenir un biome.
|
||||
- Retirer la montgolfière, son placement et ses réservations ; conserver les
|
||||
deux navires marchands et les deux îlots minéraux, dont un seul à cascade.
|
||||
- Reprendre directement `minecraft:shipwreck/rightsideup_full`, une épave
|
||||
vanilla complète déjà à l’endroit, puis restaurer les trous, les mâts et
|
||||
l’accès. Conserver ses blocs, dalles, escaliers et sa cabine, au lieu de
|
||||
dessiner une nouvelle coque approximative. Garder les marchands et un
|
||||
coffre de cargaison par navire.
|
||||
- Répartir des secteurs de marais en trois dimensions dans les souterrains
|
||||
existants, remplaçant localement les autres biomes de grottes. Le sol et les
|
||||
végétaux changent, pas la densité rocheuse. Chêne et végétation humide plutôt
|
||||
que bosquets de chêne noir dans ces secteurs.
|
||||
|
||||
Minecraft reste en **26.3-pre-2**. Les tailles restent **512 / 724 / 1 024**, Moyen
|
||||
par défaut, avec le bouton natif Personnaliser. Les options existantes des
|
||||
structures gardent leur rôle. Les marais sont naturels, indépendants de ces
|
||||
options. Le Deep Dark n'est pas ajouté.
|
||||
|
||||
Le prototype alpha.27 conserve le codec `sanctuary:island_v24` et ses ressources
|
||||
de terrain. Le créateur teste des mondes jetables et a levé l'objectif de
|
||||
compatibilité ; on ne convertit ni ne régénère ses sauvegardes. **Nouveau monde
|
||||
nécessaire.** Les archives de la 25 restent sur `codex/terrain-sanctuary-alpha25`,
|
||||
commit de livraison `df51e17` et documentation `1bf3a3b`.
|
||||
|
||||
## Ce qui reste à travailler après l'essai
|
||||
|
||||
Les plateaux étagés, l'érosion, les galeries proches des bruits de grottes
|
||||
vanilla et les sorties d'eau libres ne sont pas réintroduits en bloc. Ils
|
||||
seront examinés séparément, à partir de ce que l'on voit en jeu. Des points
|
||||
d'eau peuvent retomber sur l'île ou dans le vide ; leur intérêt ne dépend pas
|
||||
d'une arrivée dans un bassin calculé.
|
||||
|
||||
Mangroves, grenouilles, slimes et lucioles restent les références de l'écologie
|
||||
des marais. La liste réellement présente et les restrictions natives sont
|
||||
consignées dans [les vérifications](testing-alpha27.md), sans promettre une
|
||||
population observée à partir d'une simple entrée dans un biome.
|
||||
|
||||
Les fonctions propres à la 25 — nouveau village souterrain, cabane du mineur,
|
||||
cadre du sanctuaire et garantie spécifique du monument — sont écartées avec
|
||||
sa base. La 24 conserve ses structures et expansions ; leur présence n'est
|
||||
pas présentée comme une reprise de tous les contrats de la 25.
|
||||
|
||||
## Méthode retenue
|
||||
|
||||
Une passe limitée, un pack jouable, puis un retour sur son apparence. Vérifier
|
||||
le build et les nouveaux points de comportement ne remplace pas ce retour.
|
||||
Pas de matrice exhaustive ni de benchmark supplémentaire avant cet essai.
|
||||
Les prochaines optimisations devront conserver les formes jugées bonnes,
|
||||
au lieu de fabriquer des espaces faciles à certifier.
|
||||
|
||||
Le benchmark archivé de la 25 mesurait 169,816 s contre 60,932 s pour un parcours
|
||||
de 369 chunks (générations 24/25 dans les mêmes sources 25). Il documente un coût
|
||||
sur ce témoin Mac ; il ne sauve pas le rendu refusé et ne mesure pas cette 27.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Alpha.28 — roche à explorer
|
||||
|
||||
La 27 est la base validée par le créateur. Cette passe garde son île, les
|
||||
secrets minéraux et les réseaux, puis ajoute du creusement dans la roche.
|
||||
Les nouvelles cavités ne sont pas des salles de structure aux volumes prescrits.
|
||||
|
||||
## Terrain
|
||||
|
||||
Le champ commun compose les fonctions de bruit de Minecraft 26.3-pre-2 :
|
||||
cheese et cave_layer, entrées/spaghetti 3D, spaghetti 2D et roughness, piliers
|
||||
et noodle. Leur altitude est déplacée pour la masse flottante. Les coupes
|
||||
restent soustractives et s’atténuent vers les limites verticales ; il ne
|
||||
s’agit pas de recopier toute la génération Overworld ou ses aquifères.
|
||||
Les carvers historiques restent désactivés pour ne pas ajouter une seconde
|
||||
passe étrangère aux colonnes de certification.
|
||||
|
||||
Le relief supérieur déforme le volume rocheux dans les trois axes avec les
|
||||
bruits ridge/erosion natifs échantillonnés en 3D. Les déplacements sont signés :
|
||||
soulèvements, affaissements et cisaillements varient aussi avec la profondeur.
|
||||
Le déplacement est borné à ±16 blocs horizontalement et ±40 verticalement,
|
||||
avec une transition progressive de Y180 à Y228. L’enveloppe de l’île reste
|
||||
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
|
||||
Ce n’est pas une simulation physique de plaques tectoniques. Aucun cône ni
|
||||
salle géométrique ne sert de forme à ajouter au terrain.
|
||||
L’arrivée et les côtes gardent leur place. Les anciennes failles deviennent
|
||||
obliques, interrompues et limitées en hauteur, avec de la roche au-dessus et
|
||||
en dessous. Les expansions utilisent la même source de terrain ; leur climat
|
||||
et leur déformation propre s’appliquent ensuite.
|
||||
|
||||
Le seuil du champ de marais passe de 0,04 à 0,25 : les anciens secteurs se
|
||||
réduisent au profit des biomes de grottes précédents, dont les champignons.
|
||||
La hauteur du monde, les nuages et les trois diamètres sont conservés.
|
||||
|
||||
## Exploration et ressources
|
||||
|
||||
Les bateaux vanilla restaurés recherchent désormais les vraies berges proches.
|
||||
Les deux îlots sont au-dessus de Sanctuary, avec un espace libre sous leur
|
||||
roche et un seul exutoire d’eau. Leurs corps et leurs chutes d’eau ont des
|
||||
emprises distinctes ; rien ne réserve toute la colonne sous un îlot sec.
|
||||
|
||||
Les mines emploient les pièces vanilla, leurs supports, rails, coffres et
|
||||
spawners. Une partie exposée peut passer si l’ensemble reste ancré. Les huttes
|
||||
cherchent aussi des berges souterraines de marais : sol local, couverture de
|
||||
la cavité et quatre pilotis natifs. Les salles à spawner sont de vraies
|
||||
MonsterRoomFeature, posées sur des candidats réels et soumises aux refus
|
||||
natifs. Leur présence n’est pas garantie sur chaque île.
|
||||
|
||||
Une passe de filons native complète les distributions existantes dans les
|
||||
chunks de l’île et des expansions. Charbon, fer, cuivre, or, lapis, redstone,
|
||||
diamant et émeraude utilisent les formes OreFeature vanilla. La profondeur
|
||||
se mesure dans l’enveloppe rocheuse entière de la colonne, sans recommencer
|
||||
le calcul à chaque plafond de grotte. Les stocks restent finis. Les minerais
|
||||
du Nether restent dans le Nether ; le Deep Dark n’est pas ajouté à l’île.
|
||||
|
||||
## Livraison de test
|
||||
|
||||
Mod et pack `0.1.0-alpha.28`, Minecraft `26.3-pre-2`, codec de prototype
|
||||
`sanctuary:island_v24` conservé. Créer un nouveau monde. Aucune migration,
|
||||
aucune régénération ni intervention dans les sauvegardes personnelles.
|
||||
L’alpha.27 et ses artefacts restent disponibles. Les vérifications réelles
|
||||
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
|
||||
@@ -1,56 +0,0 @@
|
||||
# Alpha.29 — corniches fines et traversées présentes
|
||||
|
||||
La 28 a été validée visuellement par le créateur sur Windows, avec un chargement
|
||||
correct. Cette livraison conserve cette base et traite trois observations :
|
||||
la grande traversée absente en Grand, le relief un peu trop lissé et les mines
|
||||
vanilla difficiles à rencontrer.
|
||||
|
||||
## Témoin signalé
|
||||
|
||||
Grand **1 024**, graine **-7228211907433324401**. La reproduction avec le code
|
||||
28 confirme un plan de transit vide : 419 alignements examinés, 13 000 colonnes,
|
||||
75,114 secondes de recherche et aucun couple de portiques naturels accepté.
|
||||
Dans les douze premiers candidats natifs examinés, aucune mine n’est admise.
|
||||
Le résultat ne prétend pas inventorier les parties inconnues de toute l’île.
|
||||
|
||||
## Relief
|
||||
|
||||
La déformation 3D de la 28 reste en place. Une modulation fine du Y échantillonné
|
||||
forme de petites corniches avec des ruptures plus franches. Période locale de
|
||||
6 à 9 blocs et phase variables avec les bruits 3D existants, décalage supplémentaire
|
||||
borné à ±2,5 blocs. Aucune grille de plateaux horizontaux commune à toute l’île.
|
||||
Le raccord commence à Y214 et atteint sa pleine amplitude à Y244, à l’écart
|
||||
de l’arrivée et du contour. Les densités sous Y214 sont exactement celles de
|
||||
la 28 ; les grottes, failles, minerais, marais et lieux aériens sont conservés.
|
||||
Le même champ sert aux colonnes, chunks, biomes et certifications.
|
||||
|
||||
## Infrastructures et mines
|
||||
|
||||
Le correctif de traversée conserve le génie civil et les galeries industrielles
|
||||
existants. Les accès doivent s’adapter au relief sans transformer la surface en
|
||||
une grande fondation. Deux accès relient une chaussée et ses trottoirs ; la
|
||||
branche vers une salle conserve une paroi pleine du côté fermé de sa jonction.
|
||||
Les modules déjà certifiés ne sont plus refiltrés par un
|
||||
biome souterrain incompatible avec leur ancien tag de structure.
|
||||
|
||||
Pour les mines, les protections sont vérifiées autour des vraies pièces et de
|
||||
leurs appuis, et non dans tout le rectangle vide entre leurs branches. Le
|
||||
graphe, les pièces, les rails, coffres, toiles et spawners restent vanilla.
|
||||
Un sol rocheux portant déjà la galerie est reconnu comme appui natif ; il ne
|
||||
nécessite pas un second pilier ou une chaîne. Les galeries exposées gardent les
|
||||
ancrages bornés du vanilla et les réservations protègent aussi leurs appuis.
|
||||
Les trois essais d’altitude sont choisis dans les couches rocheuses réellement
|
||||
présentes autour des pièces, après un sondage de 32 colonnes au maximum. Les
|
||||
croisements utilisent le contrat de leur plancher vanilla : une voûte rocheuse
|
||||
proche n’est pas nécessaire à un pont de planches portant sur un corridor ancré.
|
||||
Les marches creusées et les salles gardent leurs contrôles de couverture.
|
||||
Les mines restent conditionnelles : le correctif n’impose pas une mine à chaque
|
||||
graine et ne modifie pas la politique des autres familles de structures.
|
||||
|
||||
## Distribution
|
||||
|
||||
Mod et pack **0.1.0-alpha.29**, Minecraft **26.3-pre-2**, codec de prototype
|
||||
`sanctuary:island_v24`, mêmes tailles et bouton Personnaliser. Les options de
|
||||
structures gardent leur autorité. Créer un nouveau monde ; aucune modification
|
||||
ou régénération d’une sauvegarde personnelle. Les artefacts28 restent accessibles.
|
||||
Les constats mesurés et les limites sont consignés dans [les essais](testing-alpha29.md).
|
||||
@@ -1,46 +0,0 @@
|
||||
# Alpha.30.1 — retour aux rivières courbes et au relief global
|
||||
|
||||
Correctif demandé après l’essai de la 30 : les nouvelles rivières angulaires
|
||||
évoquaient des canaux construits, et la limitation du relief à trois secteurs
|
||||
rendait trop de surface plate.
|
||||
|
||||
## Changements
|
||||
|
||||
- Suppression du générateur de rivière sur grille ajouté en 30 et de son plan
|
||||
global. L’hydrologie retrouve exclusivement les rivières courbes, bassins,
|
||||
terrasses et sources déjà présents dans `PopulationHydrology`.
|
||||
- Retour de la déformation 3D signée de la 28 sur l’ensemble du terrain, sauf
|
||||
une bande étroite de plateaux naturels vers Z = −rayon/4. Elle suit un axe
|
||||
d’implantation possible des accès de traversée. La transition est progressive ;
|
||||
le champ antérieur y est retrouvé sans dalle ajoutée ni remplissage des grottes.
|
||||
Les tests synthétiques gardent le poids global exact sur plus de 85 % de
|
||||
l’emprise circulaire, et un cœur de plateau sur moins de 8 %.
|
||||
- Brèches recherchées dans les **berges des anciens cours d’eau**, avec priorité
|
||||
aux rivières, puis aux lacs et petits bassins. Les berges sèches déjà garnies
|
||||
de sédiments ne bloquent plus cette recherche, contrairement à la 30.
|
||||
- Chaque brèche est longue de cinq blocs maximum et large de trois blocs.
|
||||
Son fond descend légèrement pour laisser l’écoulement vanilla atteindre la
|
||||
lèvre. Elle ne crée ni nouvelle source, ni rivière supplémentaire, ni
|
||||
fondation sous la chute. Aucune vérification d’un bassin de réception :
|
||||
l’eau peut descendre sur l’île ou dans le vide.
|
||||
- Jusqu’à six brèches espacées par région, uniquement lorsqu’une rive est
|
||||
assez proche d’un espace ouvert. Un bassin situé loin du bord reste fermé.
|
||||
|
||||
Les tracés courbes ne sont pas redessinés par la passe de brèche. Le retour du
|
||||
relief global peut toutefois changer leur implantation d’une version à l’autre,
|
||||
puisqu’ils recherchent leur place dans la roche disponible.
|
||||
|
||||
Les corrections de traversées, le sanctuaire minéral, les mines vanilla,
|
||||
les navires restaurés et les deux îlots restent présents. La 30.1 conserve
|
||||
Minecraft **26.3-pre-2**, les tailles **512 / 724 / 1 024**, Moyen par défaut,
|
||||
et le bouton natif Personnaliser. Aucune nouvelle interface à traduire.
|
||||
|
||||
## Essai et distribution
|
||||
|
||||
Version mod et pack : **0.1.0-alpha.30.1**, branche
|
||||
`codex/curved-rivers-alpha30-1`. **Créer un nouveau monde** : le prototype
|
||||
conserve `sanctuary:island_v24` ; aucune migration ou régénération de sauvegarde
|
||||
personnelle n’est effectuée. Les structures conservent leurs options existantes.
|
||||
|
||||
Résultats effectivement vérifiés et coordonnées dans
|
||||
[les essais](testing-alpha30.1.md). La distribution suit [packwiz](packwiz.md).
|
||||
@@ -1,50 +0,0 @@
|
||||
# Alpha.30.2 — corniches irrégulières et petits fragments
|
||||
|
||||
Retour visuel du 12 septembre 2026 : conserver les grandes masses et les arches
|
||||
acceptées, mais réduire leurs contours trop réguliers et les longues pentes
|
||||
en marches répétées. Le rendu des captures sert de contre-exemple ; il ne
|
||||
s’agit pas de remplacer les massifs par de nouvelles formes prédéfinies.
|
||||
|
||||
## Passe locale
|
||||
|
||||
Le relief global de la 28/30.1 reste actif, ainsi que sa zone limitée de plateau.
|
||||
Une adaptation locale du point d’échantillonnage vertical crée de courtes
|
||||
corniches : leur phase et leur espacement suivent un bruit 3D existant, à
|
||||
l’échelle de quelques dizaines de blocs. Les couches ne partagent pas un même
|
||||
niveau horizontal à travers toute l’île. Le déplacement supplémentaire du
|
||||
point d’échantillonnage reste borné à **2,25 blocs**.
|
||||
|
||||
La passe s’atténue sous Y188 et disparaît sous Y140, près de l’arrivée, dans le
|
||||
cœur de la bande de plateau, à l’extrémité du contour et au-dessus du terrain.
|
||||
Les volumes profonds et les paramètres des grottes natives ne sont pas changés.
|
||||
Les rivières courbes, sorties de berge et leurs écoulements gardent leur
|
||||
algorithme. Comme pour toute modification de densité, la recherche d’un lieu
|
||||
compatible peut aboutir à des coordonnées différentes sur une même graine.
|
||||
|
||||
## Petits blocs flottants
|
||||
|
||||
Une recherche de connexion dans le **champ naturel avant décoration** retire
|
||||
les composants détachés de **32 blocs au maximum** dans la zone retouchée.
|
||||
Elle s’arrête dès qu’elle rejoint 33 blocs connectés ou la roche conservée hors
|
||||
retouche. Les six faces servent de voisinage ; les diagonales ne suffisent pas.
|
||||
Les coordonnées sont globales : les bords de chunks ne coupent pas une attache.
|
||||
|
||||
Ce contrôle ne remplit aucun trou, ne rajoute aucun pilier, ne visite aucune
|
||||
sauvegarde personnelle et ne retire pas les îlots aériens. Il précède la pose
|
||||
ultérieure des arbres, structures et fluides. Il ne constitue pas une promesse
|
||||
qu’aucun élément flottant de quelque origine que ce soit ne puisse exister.
|
||||
Le cache est borné, propre au thread et invalidé selon le sampler et son contexte,
|
||||
avec des références faibles aux identités de monde.
|
||||
|
||||
## Livraison
|
||||
|
||||
Version mod et pack **0.1.0-alpha.30.2**, branche
|
||||
`codex/irregular-relief-alpha30-2`. Nouveau champ de codec optionnel
|
||||
`surface_302`, faux lorsqu’il est absent. Activé dans les trois profils actuels
|
||||
512 / 724 / 1 024 ; Moyen reste le choix par défaut. Le prototype garde
|
||||
`sanctuary:island_v24`, Minecraft 26.3-pre-2 et le bouton natif Personnaliser.
|
||||
**Créer un nouveau monde pour essayer le rendu** ; aucune régénération des
|
||||
sauvegardes personnelles. Pas de nouvelle interface nécessitant une traduction.
|
||||
|
||||
Les vérifications et limites sont dans [les essais](testing-alpha30.2.md).
|
||||
La publication et la synchronisation suivent [packwiz](packwiz.md).
|
||||
@@ -1,59 +0,0 @@
|
||||
# Alpha.30.3 — plateaux et grands reliefs
|
||||
|
||||
## Choix retenu
|
||||
|
||||
Le créateur a rejeté la 30.2 après essai : la retouche fine produisait un aspect
|
||||
rongé et trop bruité. Son nombre réduit de longues marches ne garantissait pas
|
||||
un meilleur paysage. Cette version désactive `surface_302` et `terraces_29`.
|
||||
|
||||
La roche reste le champ volumétrique Minecraft de la base alpha.24, accompagné
|
||||
des grottes et fractures adaptées en 28. Un bruit de grande longueur d’onde
|
||||
sélectionne des provinces montagneuses ; il ne dessine ni cônes ni salles.
|
||||
Les provinces sont échantillonnées dans un repère proportionnel au rayon :
|
||||
Petit 512, Moyen 724 et Grand 1 024 conservent une distribution comparable,
|
||||
avec des masses physiquement plus vastes lorsque l’île grandit.
|
||||
|
||||
Le seuil médian du champ intérieur conserve une grande part des plateaux ;
|
||||
la transition vers les provinces hautes est continue. La bande de plateau
|
||||
utile aux infrastructures reste protégée, ainsi que les 48 blocs autour du
|
||||
centre. Aucun pavage périodique ou bruit supplémentaire à l’échelle du bloc.
|
||||
|
||||
Dans les provinces hautes, le domaine vertical s’étend progressivement
|
||||
au-dessus de Y170, avec les déplacements signés sur trois axes de la 28.
|
||||
Cela relève la roche existante et conserve sa structure volumétrique, ses
|
||||
surplombs et ses vides. Une extinction douce sous Y360 laisse de la place
|
||||
sous le plafond de construction Y384. La hauteur atteinte dépend du terrain
|
||||
et de la graine ; aucun sommet n’est forcé à une altitude identique.
|
||||
|
||||
Le filtre de connexité travaille sur le champ rocheux avant décoration :
|
||||
composants de 32 blocs au plus, connexions par faces, coordonnées globales
|
||||
même à cheval sur plusieurs chunks. Il ne crée aucun pilier ni roche de
|
||||
remplissage. Les grandes masses flottantes naturelles et les îlots aériens
|
||||
ne sont pas supprimés.
|
||||
|
||||
Les grottes sous Y170 conservent exactement leur densité précédente. Les
|
||||
anciens cours d’eau courbes et leurs brèches de débordement, la traversée,
|
||||
le sanctuaire minéral, les secrets et les quatre lieux aériens sont conservés.
|
||||
Leurs recherches utilisent le terrain courant ; leurs emplacements peuvent
|
||||
changer. Pas de retour de la montgolfière ou des rivières angulaires.
|
||||
Le biome `sanctuary:swamp27_caves` rejoint les biomes autorisés des réseaux
|
||||
secrets : un sanctuaire minéral déjà certifié ne doit plus être annulé au
|
||||
stade du départ natif simplement parce qu’il se trouve dans ce marais.
|
||||
La recherche des îlots refuse immédiatement les volumes trop hauts, dès le
|
||||
premier obstacle incompatible, et filtre grossièrement les emprises au-dessus
|
||||
de trop grands trous avant leur certification bloc par bloc. Après le secteur initial, les candidats parcourent tout le tour de l’île.
|
||||
La recherche reste bornée à 64 candidats par site et 16 384 colonnes au total ;
|
||||
la certification détaillée des sites acceptés est conservée.
|
||||
|
||||
## Essai
|
||||
|
||||
Minecraft 26.3-pre-2, mêmes dépendances et bouton natif Personnaliser.
|
||||
Créer un **nouveau monde Sanctuary**. Le codec reste `sanctuary:island_v24`,
|
||||
conformément au workflow de mondes de test jetables demandé par le créateur.
|
||||
Les ressources des trois profils sont mises à jour : ne pas prolonger une
|
||||
ancienne sauvegarde pour comparer des raccords. Cette livraison n’ouvre et
|
||||
ne régénère aucune sauvegarde personnelle.
|
||||
|
||||
Les prévisualisations de densité servent à examiner les silhouettes ; elles
|
||||
ne remplacent pas l’essai visuel du paysage décoré dans le client Minecraft.
|
||||
Voir [les validations réalisées](testing-alpha30.3.md).
|
||||
@@ -1,54 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,80 +0,0 @@
|
||||
# Alpha.30 — plateaux, rivières et traversées
|
||||
|
||||
Dernière passe de génération alpha demandée par le créateur. Branche
|
||||
`codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
|
||||
`8c90c61` ; cette dernière n’a pas été publiée. Minecraft **26.3-pre-2**,
|
||||
Fabric Loader **0.19.5**, Fabric API **0.160.0+26.3** conservés.
|
||||
|
||||
## Relief
|
||||
|
||||
La déformation signée par les bruits Minecraft de la 28 agit dans trois zones
|
||||
étendues, orientées par la graine. Entre elles, son poids devient nul et le
|
||||
champ de plateaux naturel précédent reprend sa place. Il s’agit de déplacer
|
||||
et d’affaisser la roche existante, pas d’ajouter trois cônes. Le petit
|
||||
terracement systématique essayé en 29 est désactivé.
|
||||
|
||||
Les cavités et failles bornées de la 28 restent actives. Aucun nouveau
|
||||
creusement ellipsoïdal, plafond du monde ou changement des nuages. La variation
|
||||
de relief est nulle sous Y180. Les tailles 512 / 724 / 1 024 et le bouton natif
|
||||
Personnaliser sont conservés, avec Moyen par défaut.
|
||||
|
||||
Ce changement vise des étendues plus calmes séparées par du relief marqué.
|
||||
Il ne garantit pas que toute surface ait exactement deux niveaux, ni que toute
|
||||
petite pointe rocheuse ait disparu. Le rendu est à juger dans un nouveau monde.
|
||||
|
||||
## Hydrologie
|
||||
|
||||
Une recherche sur la principale masse connectée cherche une rivière longue,
|
||||
évitant les fortes pentes quand un passage plus bas existe. Elle suit le terrain
|
||||
réel avec un lit de cinq blocs environ, des niveaux locaux, du gravier et de
|
||||
l’argile. Les petits bassins et réseaux précédents restent présents.
|
||||
|
||||
Quand une berge est séparée d’un espace ouvert par **cinq blocs au maximum**,
|
||||
une brèche de trois blocs de largeur ouvre le passage. Le plan cherche jusqu’à
|
||||
six sorties espacées pour la grande rivière et une sortie supplémentaire par
|
||||
réseau régional. Il n’impose pas deux cascades terminales, ni de bassin de
|
||||
réception : l’eau peut descendre sur l’île ou dans le vide. La brèche ne pose
|
||||
ni fondation ni colonne d’eau artificielle ; les ticks vanilla propagent l’eau
|
||||
depuis les sources voisines. Les emprises de chute restent réservées aux
|
||||
expansions. L’arrivée naturelle reste à l’écart du tracé.
|
||||
|
||||
La recherche travaille sur une grille puis vérifie les blocs du lit. Les
|
||||
passages trop creux peuvent interrompre ce lit et laisser fuir l’eau. Ce n’est
|
||||
pas une simulation de bassin versant avec débit conservé ou pente monotone.
|
||||
Ces ajouts concernent l’île initiale ; les profils des continents d’expansion
|
||||
ne sont pas remplacés par cette rivière.
|
||||
|
||||
## Traversée et sanctuaire minéral
|
||||
|
||||
La recherche des deux entrées répartit plus tôt ses essais entre plusieurs
|
||||
alignements au lieu d’épuiser une seule bande. Les appuis et la protection
|
||||
des secrets gardent leur autorité. La seconde passe peut omettre un pied
|
||||
optionnel protégé seulement si les autres appuis restent valides. Les modules
|
||||
industriels, escaliers, trottoirs et parois existants sont conservés.
|
||||
|
||||
Le sanctuaire garde ses 79 × 73 × 27 blocs et sa palette minérale. Après les
|
||||
48 candidats intérieurs, une seconde recherche de 48 candidats sur les flancs
|
||||
accepte davantage d’exposition, avec des attaches rocheuses et le plancher
|
||||
praticable. Un échec complet reste diagnostiqué ; la structure ne peut pas être
|
||||
posée dans un volume intégralement vide ou en écrasant un autre secret.
|
||||
Aucun boss, machine, portail actif ou dimension ajouté.
|
||||
|
||||
Les corrections de mines de la 29 sont intégrées : hauteur choisie dans la
|
||||
roche disponible, admission des pièces et appuis réels, croisements en bois
|
||||
acceptés lorsqu’ils sont portés. Les pièces, spawners, rails et butins restent
|
||||
vanilla. Leur présence reste conditionnelle aux candidats et au terrain.
|
||||
|
||||
## Satellites et sauvegardes
|
||||
|
||||
Deux navires restaurés près des rivages et deux îlots au-dessus de Sanctuary,
|
||||
dont un à cascade : modèles et règles de proximité conservés. Les points de
|
||||
vue doivent désormais éviter les sommets que l’hydrologie va creuser en bassin
|
||||
ou en brèche ; aucun bloc artificiel n’est ajouté pour soutenir un témoin.
|
||||
|
||||
Le prototype conserve `sanctuary:island_v24` et ses profils courants, conformément
|
||||
au choix de travailler sur des mondes de test jetables. **Créer un nouveau monde.**
|
||||
Il n’y a ni migration ni régénération d’une sauvegarde personnelle. Les réglages
|
||||
Générer les structures et Structures expérimentales gardent leur rôle ; la
|
||||
rivière et le relief sont des phénomènes naturels indépendants de ces options.
|
||||
|
||||
Les résultats effectivement mesurés sont dans [les vérifications](testing-alpha30.md).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 306 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,154 +0,0 @@
|
||||
# Réutiliser la génération Sanctuary 26.2
|
||||
|
||||
Audit de source du 8 septembre 2026, effectué en lecture seule dans le dépôt voisin
|
||||
`../26.2`, à la révision `da61a3b1ec4fe161a9b5ffaab0fa3111238a35b6`
|
||||
(pack `26.2.0-alpha.242`). Les chemins ci-dessous sont relatifs à ce dépôt historique.
|
||||
Ce document décrit ce qui existe dans 26.2 ; il ne prétend pas que ces éléments sont
|
||||
déjà portés dans Sanctuary Beta.
|
||||
|
||||
## Conclusion pour le premier lot
|
||||
|
||||
La base de terrain est réutilisable : du bruit Minecraft lié à la seed, une enveloppe
|
||||
verticale d'îles flottantes, puis des masques de densité. En revanche, la génération
|
||||
26.2 n'est pas un monde initialement vide avec des continents déverrouillables.
|
||||
L'archipel extérieur est généré automatiquement au-delà d'un petit anneau de vide.
|
||||
Le premier lot doit donc borner explicitement l'île centrale et garantir un départ
|
||||
sûr, avant de construire le système persistant d'expansion collective.
|
||||
|
||||
## Carte des sources récupérables
|
||||
|
||||
| Élément | Source 26.2 | Réutilisation |
|
||||
| --- | --- | --- |
|
||||
| Bruit de terrain | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json` | `minecraft:old_blended_noise`, dépendant de la seed ; aucun bloc propre au mod. |
|
||||
| Enveloppe flottante | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json` | Gradients verticaux combinés au bruit précédent. |
|
||||
| Composition finale | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json` | Référence pour l'interpolation et le mélange ; remplacer le comportement extérieur infini. |
|
||||
| Masques et relief | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/SanctuaryDensityFunctions.java` | `AnnularVoidMask`, `TectonicMassifDensity`, `TectonicVoidDensity` ; séparer les fonctions utiles du code inactif. |
|
||||
| Réglages de dimension | `sanctuary/src/main/resources/data/sanctuary/dimension_type/sanctuary.json` | Plage verticale Y=0–383, lumière et ciel ; vérifier les codecs de la version cible. |
|
||||
| Réglages de bruit | `sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json` | Air comme fluide par défaut, aquifères désactivés, niveau marin -64 ; ne pas importer ses règles de surface en bloc. |
|
||||
| Preset | `sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json` | Générateur `minecraft:noise`, biomes `minecraft:multi_noise` ; garder seulement les dimensions nécessaires au lot. |
|
||||
| Recherche du spawn | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/world/SanctuarySpawn.java` | Recherche d'un sol naturel autour de l'origine ; corriger les limites décrites plus bas. |
|
||||
| Lacs | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/SanctuaryWaterLakeFeature.java` | Extension de `LakeFeature`, avec quelques cannes à sucre ; utilisable dans un lot hydrologie. |
|
||||
| Ravins | `sanctuary/src/main/resources/data/sanctuary/worldgen/configured_carver/tectonic_ravine.json` | Configuration de carver ; revalider sur des îles finies. |
|
||||
| Prévisualisation | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/worldgen/SanctuaryBiomePreviewScreen.java` | Référence ultérieure pour visualiser la seed et la distribution des biomes. |
|
||||
|
||||
Le graphe actif 26.2 est approximativement :
|
||||
|
||||
```text
|
||||
old_blended_noise -> floating_archipelago + central_halo_mask
|
||||
|
|
||||
max(tectonic_massifs)
|
||||
|
|
||||
min(void_basins)
|
||||
|
|
||||
blend_density -> interpolated -> squeeze
|
||||
```
|
||||
|
||||
`IslandSpawnDensity` et `RadialVoidMask` restent enregistrés en Java mais ne font
|
||||
plus partie du graphe actif. L'ancienne forme sculptée utilise des bruits de hachage
|
||||
sans seed de monde passée à la fonction ; elle n'est pas un substitut fidèle au
|
||||
terrain procédural actuel.
|
||||
|
||||
## Terramix et Black Desert
|
||||
|
||||
Dans ce dépôt, **Terramix est le nom du catalogue natif de 100 biomes d'Another
|
||||
World**, répartis en dix familles de dix. Ce n'est pas une dépendance Fabric externe
|
||||
à ajouter automatiquement au pack.
|
||||
|
||||
- Catalogue auteur : `anotherworld/src/main/resources/data/anotherworld/sanctuary_biome_catalog.json`.
|
||||
- Lecture et familles climatiques : `anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/SanctuaryBiomeCatalog.java`.
|
||||
- Fabrication des biomes, végétations et géologies : `anotherworld/build.gradle`,
|
||||
notamment la section commençant par « Terramix is a native Sanctuary catalogue ».
|
||||
- Mélange avec les biomes vanilla :
|
||||
`anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java`.
|
||||
- Black Desert est un biome historique distinct, `anotherworld:black_desert`, avec
|
||||
du sable noir et une géologie sombre. Ses règles de surface se trouvent aussi dans
|
||||
`sanctuary/.../worldgen/noise_settings/sanctuary.json`.
|
||||
|
||||
Le port du seul JSON de bruit complet échouerait sans les blocs, tags et biomes
|
||||
`anotherworld:*` qu'il référence. Préférer d'abord une surface et des biomes vanilla,
|
||||
puis un lot dédié qui préserve les identifiants du catalogue. Le mixin climatique
|
||||
historique modifie le preset Overworld global : son périmètre doit être redéfini
|
||||
pour éviter d'affecter d'autres mondes par simple installation du mod.
|
||||
|
||||
Le changelog indique que Terralith a été retiré en alpha.143 et remplacé par le
|
||||
catalogue natif en alpha.145. Ne pas confondre ces deux noms ni restaurer les anciennes
|
||||
ressources Terralith lors d'un port de Terramix.
|
||||
|
||||
## Limites et défauts à traiter
|
||||
|
||||
1. **L'extérieur n'est pas vide.** Le masque annulaire actif protège un rayon nominal
|
||||
de 96 blocs, entre dans le halo sur 160 blocs et quitte celui-ci à partir de
|
||||
256 blocs sur 96 blocs, avec une distorsion de 32 blocs. Il retrouve donc
|
||||
l'archipel normal vers 352 blocs nominalement. Ce masque ne borne pas un monde.
|
||||
2. **Aucune activation de continents trouvée.** La recherche des sources Java
|
||||
Sanctuary, Another World et Master Key n'a trouvé ni registre de continents
|
||||
déverrouillés, ni commande d'expansion directionnelle. Les « continents » du
|
||||
changelog désignent le relief tectonique naturel. Un format de sauvegarde,
|
||||
des limites de taille et une politique pour les chunks déjà explorés restent
|
||||
à concevoir.
|
||||
3. **Spawn non garanti.** `SanctuarySpawn` ne cherche que dans un rayon de trois
|
||||
chunks autour de l'origine. En cas d'échec il conserve la suggestion vanilla,
|
||||
sans créer de sol sûr. Il sélectionne à nouveau le spawn global à chaque
|
||||
`SERVER_STARTED`, ce qui peut écraser le choix d'un administrateur.
|
||||
4. **Reliefs extérieurs très éloignés.** Les massifs et bassins de vide actifs ont
|
||||
`protected_radius=10000` et `protection_fade=2000`. Les copier autour d'une petite
|
||||
île ne produit donc pas de montagne proche du départ.
|
||||
5. **L'eau n'est pas une hydrologie continentale.** Le réglage emploie de l'air et
|
||||
désactive les aquifères. Les lacs sont des features ponctuelles. Aucun réseau
|
||||
de rivières flottantes ou d'océans suspendus contrôlés n'a été identifié dans
|
||||
les sources examinées.
|
||||
6. **La densité seule n'assure pas le vide final.** Les carvers, décorations et
|
||||
structures constituent des étapes distinctes. Vérifier les chunks terminés,
|
||||
y compris aux frontières de l'île, et borner aussi les placements qui pourraient
|
||||
traverser la limite.
|
||||
7. **Ordre des features sensible.** Le changelog alpha.105 rapporte un blocage
|
||||
« Loading terrain » corrigé en ordonnant la végétation lush de plafond avant
|
||||
celle au sol. Revalider les dépendances entre features lors du port des biomes.
|
||||
|
||||
Ces constats sont issus de la lecture du code et du changelog. Aucun serveur 26.2
|
||||
n'a été lancé ni modifié pendant cet audit.
|
||||
|
||||
## Historique exploitable
|
||||
|
||||
L'historique Git local n'est pas shallow, mais commence par l'import
|
||||
`94644a2` (« Import Sanctuary 26.2 collaboration workspace »). Le générateur principal
|
||||
actuel est déjà présent dans cet import : les étapes plus anciennes ne correspondent
|
||||
donc pas à des commits individuels disponibles dans ce dépôt. Leurs descriptions
|
||||
restent dans `CHANGELOG.md` et `pack/migrations/`.
|
||||
|
||||
| Repère | Intérêt |
|
||||
| --- | --- |
|
||||
| Alpha.104–106 | Introduction des massifs, lacs, ravins et hauteur de dimension 384. |
|
||||
| Alpha.108–111 | Expériences de relief extrême, puis réduction des plateaux et pentes. |
|
||||
| Alpha.116–120 | Protection centrale, passage à l'île procédurale et réduction du halo. |
|
||||
| Alpha.143–148 | Retrait de Terralith, Terramix natif, correction des transitions et prévisualisation. |
|
||||
| Alpha.189 | Provinces climatiques à deux échelles, points MultiNoise vanilla conservés. |
|
||||
| Commit `9c09664` | Port de l'île Indev finie pour la dimension Alpha ; sujet distinct du monde Sanctuary. |
|
||||
|
||||
Les profils extrêmes sont réellement présents dans
|
||||
`sanctuary/design/worldgen/alpha108_mega_tectonics/` et
|
||||
`sanctuary/design/worldgen/alpha110_steep_massifs/`. Les chemins `backups/` cités par
|
||||
le changelog sont des repères historiques ; leur disponibilité n'a pas été vérifiée.
|
||||
|
||||
## Provenance et licences déclarées
|
||||
|
||||
Le `LICENSE` racine et le manifeste Fabric Sanctuary déclarent
|
||||
`GPL-3.0-or-later`. Garder cette provenance avec les sources effectivement reprises.
|
||||
La dimension Alpha possède en plus une notice BSD 3-Clause pour l'algorithme Indev
|
||||
dans `sanctuary/THIRD_PARTY_NOTICES.md` ; elle concerne ce port spécifique.
|
||||
|
||||
`anotherworld/THIRD_PARTY_NOTICES.md` signale des textures Flower Cows dont la
|
||||
permission de redistribution publique est à clarifier. Elles ne sont pas nécessaires
|
||||
au générateur minimal et ne doivent pas être importées avec un lot de terrain.
|
||||
|
||||
## Lots proposés après l'initialisation
|
||||
|
||||
1. Île principale finie dépendante de la seed, vide extérieur, spawn durable et sûr,
|
||||
preset explicite ; validation sur plusieurs seeds et redémarrage serveur.
|
||||
2. Modèle persistant de continents : identifiant, seed, centre, orientation, taille,
|
||||
état verrouillé/déverrouillé et version de génération ; définir le traitement
|
||||
des chunks vides déjà visités avant toute commande d'activation.
|
||||
3. Hydrologie fermée et reliefs : bassins, lacs, rivières, cavités et ravins ; vérifier
|
||||
les bordures de chunks, l'écoulement et la conservation des constructions.
|
||||
4. Terramix et Black Desert avec leur géologie, leurs ressources et une distribution
|
||||
climatique mesurable ; puis structures procédurales et continents thématiques.
|
||||
-848
@@ -1,848 +0,0 @@
|
||||
# Distribution packwiz et Prism
|
||||
|
||||
Sanctuary Beta utilise une seule instance Prism, synchronisée avant chaque
|
||||
lancement par packwiz. Le canal reste à l'adresse :
|
||||
|
||||
```text
|
||||
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Les sources sont sur la branche de travail du ticket. La branche `packwiz`
|
||||
contient uniquement les manifestes de distribution produits par le build ; les
|
||||
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.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)
|
||||
est publiée depuis **`c5381b939ad548511455f5251b51443dba4bf8d5`**, branche
|
||||
`codex/irregular-relief-alpha30-2`. Elle conserve les grandes masses et les
|
||||
arches, ajoute des corniches locales irrégulières et filtre les petits
|
||||
composants rocheux détachés dans la zone retouchée. Voir
|
||||
[la génération](generation-alpha30.2.md) et [les vérifications](testing-alpha30.2.md).
|
||||
|
||||
`check build` et `assemblePack` réussissent sur Mac. Les **24 GameTests** du
|
||||
Moyen 0 et les **24 GameTests** du Grand `-7228211907433324401` passent ; les
|
||||
volumes natifs, l’ordre d’échantillonnage, les cascades et les traversées sont
|
||||
contrôlés. Le rendu complet avec végétation reste à apprécier en jeu.
|
||||
|
||||
Le [MRpack alpha.30.2](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.2/Sanctuary-0.1.0-alpha.30.2.mrpack),
|
||||
le JAR et le ZIP d’amorçage sont vérifiés localement puis par téléchargement
|
||||
public. Le canal packwiz est au commit
|
||||
**`b9eac60a25d207540efa5a9f2d2d2087b41c86f4`**.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 sont conservés.
|
||||
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**
|
||||
sur Mac, réussissent, jeu fermé. Un seul JAR Sanctuary alpha.30.2 est actif ;
|
||||
les empreintes des **925 fichiers personnels et réglages suivis** sont conservées.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30.2/`.
|
||||
|
||||
SHA-256 JAR : `a11a47f08b052703892fd9a705636863f0c9313e9101641538c1b6f37ff2873a`.
|
||||
SHA-256 MRpack : `f80cd65e10b66851a13e21fa878101c198bec3e115c26676981dc1275c735bfa`.
|
||||
Reçus : `build/alpha302-delivery-public.json`,
|
||||
`build/alpha302-isolated-validation.json`, `build/alpha302-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde**. Témoin simple : **Moyen 724, graine 0**.
|
||||
Aucun essai Windows natif ni mise à jour de l’instance Windows n’est revendiqué.
|
||||
|
||||
## Alpha.30.1 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.30.1](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.1)
|
||||
est publiée depuis **`cccb89150e7e7c7bb13713aeab579e9203ff0845`**, branche
|
||||
`codex/curved-rivers-alpha30-1`. Elle retire les nouvelles rivières sur grille,
|
||||
restaure le relief 3D global avec une bande de plateau, et ouvre de courtes
|
||||
brèches dans les berges des anciens cours d’eau. Voir
|
||||
[la génération](generation-alpha30.1.md) et [les preuves](testing-alpha30.1.md).
|
||||
|
||||
`check build` et `assemblePack` passent sur Mac. Les **24 GameTests** du témoin
|
||||
Moyen 0, puis les **24 GameTests** du Grand `-7228211907433324401` après
|
||||
rechargement, réussissent. L’écoulement des cascades est vérifié sous ticks
|
||||
réels ; les contrôles ne remplacent pas l’appréciation visuelle en jeu.
|
||||
|
||||
Le [MRpack alpha.30.1](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.1/Sanctuary-0.1.0-alpha.30.1.mrpack),
|
||||
le JAR et le ZIP d’amorçage sont vérifiés localement puis par téléchargement
|
||||
public. Le canal packwiz est au commit
|
||||
**`3d1680f9f2722304ee0ad5950cc71fa61987d2f3`**. Minecraft 26.3-pre-2,
|
||||
Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
|
||||
|
||||
Deux synchronisations isolées, puis deux dans l’instance existante
|
||||
**Sanctuary Beta** sur Mac, réussissent, jeu fermé. Un seul JAR Sanctuary
|
||||
alpha.30.1 est actif ; les empreintes des **925 fichiers personnels et réglages
|
||||
suivis** sont conservées. Sauvegarde ciblée :
|
||||
`sanctuary-backups/before-0.1.0-alpha.30.1/`.
|
||||
|
||||
SHA-256 JAR : `d719a2d7bb544299ecc4adba617ea524d898ec003df35cf3464877fb74a7d0dd`.
|
||||
SHA-256 MRpack : `37c576d868071a078619b4fd53a1a1eff9cd99ff7dbb16c146bd7360bc4e9274`.
|
||||
Reçus : `build/alpha301-delivery-public.json`,
|
||||
`build/alpha301-isolated-validation.json`, `build/alpha301-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde**. Essai conseillé : **Moyen 724, graine 0** ; sorties
|
||||
de l’ancienne rivière vers **13 246 82** et **-105 246 -65**. Aucun essai Windows
|
||||
natif de ce correctif ni modification de l’instance Windows n’est revendiqué.
|
||||
|
||||
## Alpha.30 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30)
|
||||
est publiée depuis **`bf327b7`**, branche `codex/terrain-finale-alpha30`.
|
||||
Le point de reprise local alpha.29 est inclus sans publication séparée.
|
||||
Voir [la génération](generation-alpha30.md) et [les preuves](testing-alpha30.md).
|
||||
|
||||
`check build`, les 24 GameTests natifs du témoin Grand42, son redémarrage et
|
||||
`assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP d’amorçage sont
|
||||
vérifiés localement puis par téléchargement public. Le
|
||||
[MRpack alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30/Sanctuary-0.1.0-alpha.30.mrpack)
|
||||
est disponible ; Minecraft 26.3-pre-2 et les dépendances restent inchangés.
|
||||
|
||||
Le canal packwiz est au commit **`77bc330da65f93e08e52ea9c81b73bb42f1fdd2b`**.
|
||||
Deux synchronisations isolées puis deux dans l’instance existante **Sanctuary Beta**
|
||||
sur Mac réussissent, jeu fermé. Un seul JAR Sanctuary alpha.30 est actif ;
|
||||
les empreintes des **925 fichiers personnels et réglages suivis** sont conservées.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30/`.
|
||||
|
||||
SHA-256 JAR : `ebd9e32a32a548985b0de95e1e850212a5bf03b50cfef59db143896312807d7a`.
|
||||
SHA-256 MRpack : `f029912098c823bb5e0b7609388856d46c53e89b8172d295fd975269a0e43ce6`.
|
||||
Reçus : `build/alpha30-delivery-public.json`,
|
||||
`build/alpha30-isolated-validation.json`, `build/alpha30-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde**. Les fichiers de l’instance Windows n’ont pas été
|
||||
modifiés ; aucune validation Windows native de cette version n’est revendiquée.
|
||||
La disposition des plateaux et les grandes cascades restent à apprécier en jeu.
|
||||
|
||||
## Alpha.28 publiée — 12 septembre 2026
|
||||
|
||||
La [release alpha.28](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.28)
|
||||
est publiée depuis `79d3185`. Elle prolonge la 27 avec les cavités natives,
|
||||
la déformation 3D signée du relief, des failles bornées, les mines/donjons et
|
||||
les filons, moins de marais, deux bateaux près des berges et deux îlots au-dessus
|
||||
de Sanctuary. Les huttes restent conditionnelles. Voir
|
||||
[la génération](generation-alpha28.md) et [les résultats](testing-alpha28.md).
|
||||
|
||||
`check build` passe sur Mac : **21/21 GameTests natifs**. `assemblePack`,
|
||||
JAR, MRpack et ZIP d’amorçage sont vérifiés localement puis par téléchargement
|
||||
public. Minecraft 26.3-pre-2 et les dépendances restent inchangés.
|
||||
|
||||
Le [MRpack autonome](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.28/Sanctuary-0.1.0-alpha.28.mrpack)
|
||||
est disponible. Le canal packwiz est au commit
|
||||
`6ec1c10c710870a2ccc591b524cc26d125ca03a2`. Deux synchronisations isolées,
|
||||
puis deux dans l’instance existante **Sanctuary Beta**, réussissent.
|
||||
Un seul JAR Sanctuary alpha.28 est actif ; les **925 fichiers personnels
|
||||
et réglages suivis** gardent leurs empreintes. Le jeu était fermé.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.28/`.
|
||||
|
||||
SHA-256 JAR : `7f95e9ebd50063c7ebfc649fa325e75263649e42c02e7159244953e09e27d24e`.
|
||||
SHA-256 MRpack : `12d4db8f06a13fb19feb08e61325361f7ab4d783d6f43b3d0c3582d316c54d0f`.
|
||||
Reçus : `build/alpha28-delivery-public.json`,
|
||||
`build/alpha28-isolated-validation.json`, `build/alpha28-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** ; témoin **Moyen 724, graine 0**.
|
||||
Les tests natifs concernent Mac, pas Windows. Le rendu client et les surplombs
|
||||
restent à juger en jeu. Aucun monde personnel n’a été régénéré.
|
||||
|
||||
## Alpha.27 publiée — 12 septembre 2026
|
||||
|
||||
La reprise repart directement de l’alpha.24 : relief et cavités conservés,
|
||||
marais souterrains localisés, montgolfière retirée et navires fondés sur le
|
||||
véritable template vanilla `minecraft:shipwreck/rightsideup_full`, restauré.
|
||||
Voir [le périmètre](generation-alpha27.md) et [les essais](testing-alpha27.md).
|
||||
Minecraft 26.3-pre-2 et les dépendances de la 24 sont conservés.
|
||||
|
||||
`check build` puis `assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP
|
||||
d’amorçage sont vérifiés localement puis par téléchargement public.
|
||||
|
||||
La [release alpha.27](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.27)
|
||||
est publiée depuis `392ade0`. Le canal packwiz est au commit
|
||||
`9803b2279aa5242c9e3e7202b940bf47e1c3f404`. Le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.27/Sanctuary-0.1.0-alpha.27.mrpack)
|
||||
est disponible séparément.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans la même instance
|
||||
Prism **Sanctuary Beta** réussissent. Un seul JAR Sanctuary alpha.27 est actif.
|
||||
Les **925 fichiers personnels et réglages suivis** gardent leurs empreintes ;
|
||||
le jeu était fermé. Le dossier reste `Sanctuary-0.1.0-alpha.1`, avec sauvegarde
|
||||
ciblée dans `sanctuary-backups/before-0.1.0-alpha.27/`.
|
||||
Reçus locaux : `build/alpha27-delivery-public.json`,
|
||||
`build/alpha27-isolated-validation.json`, `build/alpha27-prism-validation.json`.
|
||||
|
||||
SHA-256 JAR : `56144b291407650e148aa3769596165a5e87398ea4a847cf67352ce2a2c625bf`.
|
||||
SHA-256 MRpack : `e13be90c6454bad3cdd3418fc54b827488700876f6e4ddb42e4a05f57acfecb6`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour cet essai. Les sauvegardes et réglages
|
||||
personnels sont conservés. Aucun essai Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.24 publiée — 11 septembre 2026
|
||||
|
||||
`check build assemblePack` et les essais natifs passent. Le JAR et
|
||||
`build/Sanctuary-0.1.0-alpha.24.mrpack` sont vérifiés : versions, index,
|
||||
empreintes et JAR intégré concordent. Minecraft 26.3-pre-2, Fabric Loader
|
||||
0.19.5 et Fabric API 0.160.0+26.3 sont conservés. Voir
|
||||
[les vérifications](testing-alpha24.md) et [la génération](generation-alpha24.md).
|
||||
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
f045655cef30bbaa93099a24443462f213d7851fecd03b7b00c6ca874c9d9c45
|
||||
```
|
||||
|
||||
La [release alpha.24](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.24)
|
||||
est publiée depuis le commit source `27d7cba`. Le canal packwiz est au commit
|
||||
`bd9b4c4`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.24/Sanctuary-0.1.0-alpha.24.mrpack)
|
||||
et le ZIP d’amorçage ont été vérifiés par téléchargement public.
|
||||
|
||||
Deux installations isolées, puis deux synchronisations dans la même instance
|
||||
Sanctuary Beta réussissent. Un seul JAR Sanctuary alpha.24 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.24/`, hors de `mods/`.
|
||||
Reçus locaux : `build/alpha24-artifacts.json`,
|
||||
`build/alpha24-isolated-validation.json`, `build/alpha24-prism-validation.json`.
|
||||
Le jeu est fermé pendant la mise à jour ; aucun monde personnel n’est ouvert,
|
||||
converti ou régénéré. Le lanceur est resté ouvert et ses réglages sont inchangés.
|
||||
|
||||
Pour essayer : créer un **nouveau monde Sanctuary**, avec les structures et
|
||||
l’option expérimentale activées. Les cinq lieux sont référencés dans le
|
||||
[guide des témoins](testing-alpha24.md). Le ZIP d’amorçage suit le canal stable ;
|
||||
il ne sert pas à réimporter l’instance existante. Aucun essai Windows natif
|
||||
n’est revendiqué.
|
||||
|
||||
## Alpha.23.1 locale vérifiée — 11 septembre 2026
|
||||
|
||||
Le JAR et `build/Sanctuary-0.1.0-alpha.23.1.mrpack` sont construits et vérifiés
|
||||
après `check build` et `assemblePack`. Les versions Minecraft 26.3-pre-2,
|
||||
Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 sont conservées.
|
||||
Voir [les validations](testing-alpha23.1.md) et
|
||||
[le contrat de génération](generation-alpha23.1.md).
|
||||
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
906fd2e12be4233676a1a561556508eee7412183698c2cac704dc1191324f136
|
||||
```
|
||||
|
||||
**Publication en attente :** le gestionnaire d’identifiants Git ne fournit pas
|
||||
l’authentification nécessaire. Le canal public reste à l’alpha.22 ; aucune
|
||||
synchronisation de l’instance Prism n’a été effectuée pour l’alpha.23.1.
|
||||
Le MRpack local peut être conservé pour les essais. Le ZIP d’amorçage préparé
|
||||
suit encore le canal public alpha.22 et ne doit pas être présenté comme une
|
||||
installation publiée de l’alpha.23.1. Les étapes de publication puis de
|
||||
synchronisation ci-dessous restent à exécuter après rétablissement de l’accès.
|
||||
|
||||
## Alpha.22 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.22](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.22)
|
||||
est publiée depuis le commit source `d6e0c14`. Le canal stable est au commit
|
||||
`0019178`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.22/Sanctuary-0.1.0-alpha.22.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
|
||||
```
|
||||
|
||||
Les bâtiments de surface recherchent des terrains adaptés ; la grande traversée
|
||||
et la salle d’expansion restent des éléments de base, indépendants d’une ville.
|
||||
Les galeries peuvent chercher un détour et de nouveaux accès. Le hall peut
|
||||
accueillir des égouts ailleurs dans l’île. Les trois tailles natives, le cas OFF,
|
||||
la réouverture et la comparaison21 passent. `check build assemblePack` passe
|
||||
avec75 tâches et les huit GameTests généraux. Voir [les essais22](testing-alpha22.md).
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
|
||||
Un seul JAR Sanctuary alpha.22 est actif ; les **925 fichiers personnels et
|
||||
réglages suivis** gardent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR21 et le suivi
|
||||
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.22/`.
|
||||
Reçus locaux : `build/alpha22-isolated-validation.json` et
|
||||
`build/alpha22-prism-validation.json`.
|
||||
|
||||
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**. Les portiques de la
|
||||
traversée sont `-170 253 -98` et `170 254 -98` ; la trappe du hall indépendant
|
||||
est `2 252 0`. `/sanctuary structures` liste les lieux. Les villes et bâtiments
|
||||
facultatifs peuvent être refusés. Les graines vérifiées ne prouvent pas la
|
||||
présence des infrastructures sur toutes les graines ; les recherches restent
|
||||
bornées. Aucun monde personnel n’a été ouvert, converti ou régénéré. Aucun
|
||||
essai client visuel ni essai Windows natif n’est revendiqué.
|
||||
|
||||
## Alpha.21 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.21](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.21)
|
||||
est publiée depuis le commit source `bb63fab`. Le canal stable utilisait cette version au commit
|
||||
`8002fce`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.21/Sanctuary-0.1.0-alpha.21.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
|
||||
```
|
||||
|
||||
Palettes sobres, toitures portées, quartiers reliés, placements patrimoniaux
|
||||
assouplis et premières galeries industrielles avec fronts rocheux et filons.
|
||||
Voir le [catalogue](architecture-alpha21.md) et les [essais](testing-alpha21.md).
|
||||
`check build assemblePack`, les huit GameTests généraux, les créations et
|
||||
réouvertures natives passent. Le cas expérimental OFF passe. Les fichiers
|
||||
historiques et les témoins natifs de la génération20 sont conservés.
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism réussissent.
|
||||
Un seul JAR Sanctuary alpha.21 est actif ; les **925 fichiers personnels et
|
||||
réglages suivis** gardent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR20 et le suivi
|
||||
packwiz sont sauvegardés sous `sanctuary-backups/before-0.1.0-alpha.21/`.
|
||||
Reçus locaux : `build/alpha21-isolated-validation.json` et
|
||||
`build/alpha21-prism-validation.json`.
|
||||
|
||||
Pour tester : **nouveau monde Sanctuary, Moyen, graine0**, **Structures
|
||||
expérimentales** activées dans **Personnaliser**. Entrées des galeries :
|
||||
`-135 253 -178` et `-135 249 162`. `/sanctuary structures` liste les lieux.
|
||||
Les raccords dépendent du relief ; certains sites restent isolés et les réseaux
|
||||
souterrains ne sont pas tous reliés. Aucun monde personnel n’a été ouvert,
|
||||
converti ou régénéré. Aucun essai Windows natif ni essai visuel client n’est
|
||||
revendiqué. Une sauvegarde21 nécessite le binaire21 ou un futur binaire compatible.
|
||||
|
||||
## Alpha.20 publiée — 10 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.20](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.20)
|
||||
est publiée depuis le commit source `0edac29`. Le canal stable utilisait cette
|
||||
version au commit `4e0967d`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.20/Sanctuary-0.1.0-alpha.20.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
9ea10576e641d83c13e1277960a375448a22d416a98a6b327c4d6d2a204c423a
|
||||
```
|
||||
|
||||
Passe de détails sur six bâtiments de ville, quatorze infrastructures,
|
||||
espaces publics et réseaux souterrains : palettes propres aux usages,
|
||||
boiseries, tuf ciselé, corniches, biseaux, verrières et raccords des vitres.
|
||||
Voir le [catalogue illustré](architecture-alpha20.md).
|
||||
|
||||
`check build assemblePack` passe, avec huit GameTests généraux. Les créations
|
||||
et réouvertures natives conservent les structures, accès, supports et coffres.
|
||||
Le bassin et les sorties d’égout restent fonctionnels. Les rapports de la
|
||||
version19 restent identiques à ceux de la release publiée. Le mode désactivé
|
||||
et une petite île passent ; les quatorze infrastructures sont rencontrées sur
|
||||
les deux témoins. Les limites des raccords au terrain sont détaillées dans
|
||||
[les preuves alpha.20](testing-alpha20.md).
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.20 est actif ; les **925 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ». Le JAR19 et le suivi
|
||||
packwiz sont sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.20/`.
|
||||
Reçus locaux : `build/alpha20-isolated-validation.json` et
|
||||
`build/alpha20-prism-validation.json`.
|
||||
|
||||
Pour voir cette architecture, créer un **nouveau monde Sanctuary, Moyen,
|
||||
graine 0**, avec **Structures expérimentales** activées dans **Personnaliser**.
|
||||
`/sanctuary structures` liste les entrées. Petit, graine42, possède la
|
||||
cartothèque à `-101 251 -25`. L’option désactivée d’un autre nouveau monde
|
||||
conserve la ville sans les réseaux expérimentaux. Les anciens mondes gardent
|
||||
leur génération ; aucune sauvegarde personnelle n’a été ouverte, convertie ou
|
||||
régénérée. Une sauvegarde20 ne se rétrograde pas vers le binaire19.
|
||||
|
||||
Les vues du catalogue viennent des modèles et ne sont pas des captures de
|
||||
partie. Aucun essai client visuel ni essai Windows natif n’a été effectué.
|
||||
Les nouvelles fonctions de la bêta restent à construire.
|
||||
|
||||
## Alpha.19 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.19](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.19)
|
||||
est publiée depuis le commit source `c23e72b`. Le canal stable utilisait alors
|
||||
cette version, au commit `ea883b7`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.19/Sanctuary-0.1.0-alpha.19.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
a159b00a2778020800e81381e0608495a4d7909f1f853f0b8812cd4017865b0b
|
||||
```
|
||||
|
||||
Première passe de quatorze architectures et de leurs voies de fret ou chemins
|
||||
abandonnés sur l’île initiale. Les trois tailles sont vérifiées nativement ;
|
||||
les quatorze types ont chacun été rencontrés et parcourus sur ces témoins.
|
||||
Créer un **nouveau monde Sanctuary, Moyen, graine 0**, puis garder
|
||||
**Structures expérimentales** activées dans **Personnaliser** : ce témoin
|
||||
contient treize lieux et sept liaisons, dont un fret de 499 blocs.
|
||||
`/sanctuary structures` relève les entrées. La présence des bâtiments et les
|
||||
raccords dépendent du relief ; voir [les preuves et la carte](testing-alpha19.md).
|
||||
|
||||
`check build assemblePack` et les huit GameTests généraux passent.
|
||||
Création et réouverture natives conservent 112 pièces et 13 coffres ; 68 cibles
|
||||
sont accessibles. Le mode désactivé passe. Les empreintes de la génération 17
|
||||
restent identiques aux preuves publiées, y compris après réouverture.
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.19 est actif ; les **767 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
|
||||
personnelle n’a été ouverte, convertie ou régénérée. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
|
||||
|
||||
Le JAR 17 et le suivi packwiz sont sauvegardés dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.19/`. Reçus locaux ignorés :
|
||||
`build/alpha19-isolated-validation.json` et `build/alpha19-prism-validation.json`.
|
||||
|
||||
Pour comparer, créer un autre nouveau monde avec l’option désactivée : le socle
|
||||
urbain demeure, sans les expériences 17/19. Cette option ne supprime pas de
|
||||
structures dans une sauvegarde existante. Les anciens mondes conservent leur
|
||||
génération ; une sauvegarde 19 ne doit pas être ouverte avec l’ancien binaire 17.
|
||||
Les bâtiments préparent les futurs usages, mais mailbox, caméra, réacteur et
|
||||
progression restent pour la bêta. Aucun essai visuel dans le client ni essai
|
||||
Windows natif n’a été effectué.
|
||||
|
||||
## Alpha.17 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.17](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.17)
|
||||
est publiée depuis le commit source `0969ed5`. Le canal stable utilisait alors cette
|
||||
version, au commit `6e0c297`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.17/Sanctuary-0.1.0-alpha.17.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
9ab7a9f03a46bc940d6bc014de85c6935eb5ba21f11ceea9b1222a9dae1faaec
|
||||
```
|
||||
|
||||
L’option **Structures expérimentales** de **Personnaliser** ajoute trois familles
|
||||
de petits réseaux sur les nouvelles îles initiales : mines et ateliers, salles
|
||||
étranges, installations abandonnées. Le choix est enregistré avec la taille.
|
||||
Le témoin **Moyen, graine 0**, option activée, possède trois réseaux accessibles
|
||||
depuis la surface. Voir [les preuves et coordonnées](testing-alpha17.md).
|
||||
|
||||
`check build assemblePack`, les huit GameTests et les trois smokes dédiés passent.
|
||||
Création et réouverture natives conservent 15 pièces, 3 coffres, 12 appuis et le
|
||||
bloc témoin ; 49 cibles sont accessibles. Le cas désactivé passe. La génération 16
|
||||
conserve les empreintes de ses structures et de son journal publiés.
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.17 est actif ; les **767 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
|
||||
personnelle n’a été ouverte, convertie ou régénérée. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
|
||||
|
||||
Le JAR 16 et le suivi packwiz sont sauvegardés dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.17/`. Reçus locaux ignorés :
|
||||
`build/alpha17-isolated-validation.json` et `build/alpha17-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour cet essai. L’option désactivée permet
|
||||
un autre nouveau monde sans les réseaux supplémentaires ; elle ne retire pas
|
||||
de blocs d’un monde existant. Les anciennes sauvegardes gardent leur génération.
|
||||
Ne pas ouvrir une sauvegarde 17 avec le binaire 16. Les connexions entre réseaux,
|
||||
le réacteur, les reliques et la progression restent à construire. Aucun essai
|
||||
Windows natif ni revue visuelle dans le client n’a été effectué.
|
||||
|
||||
## Alpha.16 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.16](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.16)
|
||||
est publiée depuis le commit source `98fc13c`. Le canal stable utilisait alors cette
|
||||
version, au commit `927c862`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.16/Sanctuary-0.1.0-alpha.16.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
5504018c46300accf01478dbc8bfb8e216395a5435cf7d7f501005cbc65b8e43
|
||||
```
|
||||
|
||||
`check build assemblePack`, les huit GameTests et les scénarios natifs de
|
||||
création/réouverture passent. Six reliefs contrôlés possèdent une ville ; les
|
||||
grands équipements souterrains restent conditionnés par les emplacements
|
||||
admissibles trouvés dans le budget de génération. Le témoin Moyen, graine 0,
|
||||
contient la piscine de 50 × 25, huit niveaux de parking, la grande salle sèche
|
||||
et un égout ouvert avec écoulement vérifié. Voir [les preuves et coordonnées](testing-alpha16.md).
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac
|
||||
réussissent. Un seul JAR Sanctuary alpha.16 est actif ; les **767 fichiers
|
||||
personnels et réglages suivis** conservent leurs hashes. Aucune sauvegarde
|
||||
personnelle n’a été ouverte, convertie ou régénérée. Le dossier reste
|
||||
`Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
|
||||
|
||||
Le JAR15 et le suivi packwiz sont sauvegardés dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.16/`. Les reçus locaux ignorés sont
|
||||
`build/alpha16-isolated-validation.json` et `build/alpha16-prism-validation.json` ;
|
||||
les empreintes avant/après figurent aussi dans la sauvegarde ciblée de l’instance.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour les nouveautés16. Les mondes publiés
|
||||
jusqu’à15 gardent leur génération, y compris leurs futurs chunks. La machine
|
||||
d’expansion, le réacteur et les nouvelles familles de donjons restent prévus.
|
||||
Aucun accès à l’instance Windows n’a eu lieu pour cette livraison.
|
||||
|
||||
## Alpha.15 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.15](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.15)
|
||||
est publiée depuis le commit source `dd9c795`. Le canal stable utilisait alors cette
|
||||
version, au commit `6cf54ee`. JAR, [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.15/Sanctuary-0.1.0-alpha.15.mrpack)
|
||||
et ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
52253f386d30530cc718d4b90ac6953cd4790b2125c78b74599eebff944824f2
|
||||
```
|
||||
|
||||
`check build assemblePack`, les huit GameTests et les scénarios natifs de
|
||||
création/réouverture passent. Deux synchronisations isolées puis deux dans la
|
||||
même instance Prism Mac réussissent. Un seul JAR Sanctuary alpha.15 est actif ;
|
||||
les **767 fichiers personnels et réglages suivis** conservent leurs hashes.
|
||||
Aucune sauvegarde personnelle n’a été ouverte, convertie ou régénérée.
|
||||
Le dossier reste `Sanctuary-0.1.0-alpha.1`, affiché « Sanctuary Beta ».
|
||||
|
||||
Le JAR d’essai15 ajouté manuellement avant publication est conservé dans
|
||||
`sanctuary-backups/alpha15-manual-preview/`, hors de `mods/`. Le JAR14 et le
|
||||
suivi packwiz sont sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.15/`.
|
||||
Les reçus locaux ignorés sont `build/alpha15-isolated-validation.json` et
|
||||
`build/alpha15-prism-validation.json` ; les empreintes avant/après figurent
|
||||
aussi dans la sauvegarde ciblée de l’instance.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour la ville compacte et ses sous-sols,
|
||||
y compris après l’essai anticipé15. Les mondes publiés12/13/14 gardent leurs
|
||||
générateurs. La salle d’expansion est présente lorsque le terrain le permet ;
|
||||
sa future machine et les cartes préremplies ne sont pas implémentées.
|
||||
Voir [les preuves, coordonnées et limites](testing-alpha15.md).
|
||||
Aucun accès à l’instance Windows n’a eu lieu pour cette livraison.
|
||||
|
||||
## Alpha.14 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.14](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.14)
|
||||
est publiée depuis le commit source `1c5c2c2`. Le canal stable utilisait alors cette
|
||||
version, au commit `dabf57e`. Le JAR, le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.14/Sanctuary-0.1.0-alpha.14.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été vérifiés par téléchargement public.
|
||||
SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
1c517a13e1151501c6f4974d1bc00faedf67aaecd9bf58f34ac4a655d09dc6ef
|
||||
```
|
||||
|
||||
`check build assemblePack` et les huit GameTests passent, ainsi que les
|
||||
scénarios natifs Lost City et reprise du vide après réouverture. Deux
|
||||
synchronisations isolées puis deux dans la même instance Prism Mac ont réussi.
|
||||
Le second passage conserve les fichiers gérés à l’identique ; un seul JAR
|
||||
Sanctuary alpha.14 est actif. Les **713 fichiers personnels et réglages suivis**
|
||||
conservent leurs hashes. Aucune sauvegarde personnelle n’a été ouverte,
|
||||
convertie ou régénérée. Le dossier reste `Sanctuary-0.1.0-alpha.1`.
|
||||
|
||||
Sauvegarde ciblée dans cette instance :
|
||||
`sanctuary-backups/before-0.1.0-alpha.14/`. Reçus locaux ignorés :
|
||||
`build/alpha14-isolated-validation.json` et `build/alpha14-prism-validation.json`.
|
||||
Voir le [contrat de génération et les preuves](testing-alpha14.md).
|
||||
La machine Windows n’a pas été accessible pour un essai natif ou une installation.
|
||||
|
||||
La livraison conserve la même instance et ses sauvegardes. Pour les nouveautés
|
||||
Lost City, créer ensuite un **nouveau monde Sanctuary** de génération 14 ;
|
||||
mettre à jour le pack ne convertit pas les mondes 12/13. Dans ce nouveau monde
|
||||
uniquement, une expansion réservée sur du vide certifié peut demander de fermer
|
||||
et rouvrir le monde avant sa préparation. Ce mécanisme ne fait pas partie de
|
||||
l’installateur packwiz et n’autorise pas à supprimer un monde ou son journal.
|
||||
|
||||
## Correctif alpha.13.1 publié — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.13.1](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.13.1)
|
||||
est publié depuis le commit source `b19e93d`. Le canal stable utilisait alors cette
|
||||
version. Le JAR, le [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.13.1/Sanctuary-0.1.0-alpha.13.1.mrpack)
|
||||
et le ZIP d’amorçage ont été vérifiés par téléchargement public. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
b340dfc58f3d2d1a2a375dc527ac6721c61430600622046db26228e0da46061b
|
||||
```
|
||||
|
||||
Deux synchronisations isolées puis deux dans la même instance Prism Mac ont
|
||||
réussi. Le second passage conserve les fichiers gérés à l’identique ; un seul
|
||||
JAR Sanctuary alpha.13.1 est actif. Les **713 fichiers personnels et réglages
|
||||
suivis** conservent leurs hashes. Aucune sauvegarde personnelle n’a été ouverte,
|
||||
convertie ou régénérée. Dossier de l’instance conservé : `Sanctuary-0.1.0-alpha.1`.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.13.1/` ; reçus locaux
|
||||
`build/alpha131-isolated-validation.json` et `build/alpha131-prism-validation.json`.
|
||||
|
||||
Le correctif évite la synchronisation de dossier non prise en charge sous
|
||||
Windows. **Aucun nouveau monde requis :** fermer Minecraft, mettre l’instance
|
||||
à jour puis rouvrir le même monde. Les réservations enregistrées reprennent
|
||||
après vérification. La machine Windows n’a pas été accessible pour installation
|
||||
ou essai natif : le [guide](alpha13-worldgen.md#correctif-alpha131--expansions-sous-windows)
|
||||
décrit aussi le remplacement du seul JAR Sanctuary pour une instance issue du
|
||||
MRpack sans auto-update. Voir les [preuves et limites](testing.md#correctif-alpha131--journal-dexpansion-sous-windows).
|
||||
|
||||
## Alpha.13 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.13](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.13)
|
||||
est publiée depuis le commit source `78b63be`. Le canal stable utilisait alors
|
||||
cette version. Le JAR, le [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.13/Sanctuary-0.1.0-alpha.13.mrpack)
|
||||
et le ZIP d’amorçage ont été vérifiés par téléchargement public. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
53eb5d8eda54d322250784203824993bd675de1cb90467e74d67589b15769e76
|
||||
```
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans la même instance Prism
|
||||
ont réussi. Le second passage laisse les fichiers gérés identiques ; un seul
|
||||
JAR Sanctuary alpha.13 est actif. Les **601 fichiers personnels et réglages
|
||||
suivis** conservent leurs hashes, notamment les mondes et mods tiers. Aucune
|
||||
sauvegarde personnelle n’a été ouverte, convertie ou régénérée.
|
||||
|
||||
L’instance conserve son dossier `Sanctuary-0.1.0-alpha.1`, sans réimportation.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.13/` dans cette instance.
|
||||
Reçus locaux : `build/alpha13-isolated-validation.json` et
|
||||
`build/alpha13-prism-validation.json`.
|
||||
|
||||
Pour tester : créer un **nouveau monde**, choisir **Sanctuary**, puis le bouton
|
||||
natif **Personnaliser** : Petit 512, Moyen 724 par défaut, Grand 1 024.
|
||||
Le laboratoire n’est plus un choix public ; ses cavités et outils d’expansion
|
||||
rejoignent le nouveau monde. Les anciennes générations restent chargeables.
|
||||
Voir [le guide alpha.13](alpha13-worldgen.md) et [les preuves](testing.md).
|
||||
|
||||
## Correctif alpha.12.1 publié — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.12.1](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.12.1)
|
||||
est publiée depuis le commit source `332d60b`. Le canal packwiz utilisait cette
|
||||
version. Le JAR, le [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.12.1/Sanctuary-0.1.0-alpha.12.1.mrpack)
|
||||
et le ZIP d’amorçage ont été vérifiés par téléchargement public. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
c9df4da956cfa54581b3fde21de006bf8ac3ec025b8dd0daa5d060bcb70aabd4
|
||||
```
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans la même instance Prism
|
||||
ont réussi ; le second laisse les fichiers gérés identiques. Un seul JAR
|
||||
Sanctuary alpha.12.1 est actif, avec **466 fichiers personnels et réglages suivis
|
||||
conservés**, y compris les mondes et les mods tiers. Aucun monde n’a été ouvert
|
||||
ou régénéré. La sauvegarde ciblée est dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.12.1/` de l’instance existante ; reçus locaux
|
||||
`build/alpha121-isolated-validation.json` et `build/alpha121-prism-validation.json`.
|
||||
|
||||
WG-14 ajoute la distance et le contournement automatique des zones occupées.
|
||||
Les mondes laboratoire alpha.12 restent utilisables sans conversion : le journal
|
||||
v1 et les descripteurs de génération 12 sont conservés. Voir le [guide](alpha12-laboratory.md).
|
||||
|
||||
## Alpha.12 publiée — 9 septembre 2026
|
||||
|
||||
[Sanctuary 0.1.0-alpha.12](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.12)
|
||||
est publiée depuis le commit source `cdfbba0`. Le canal stable utilisait alors
|
||||
cette version. Le JAR, le [MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.12/Sanctuary-0.1.0-alpha.12.mrpack)
|
||||
et le ZIP d’amorçage Prism ont été téléchargés publiquement et vérifiés contre
|
||||
les artefacts locaux. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
f2ac2462a03a39ef330cc52143f9753ac5908b606cfe1dd1388ad5caae15e6e9
|
||||
```
|
||||
|
||||
Le véritable installateur packwiz a réussi deux passages dans un dossier de
|
||||
développement neuf, puis deux dans la même instance Sanctuary Beta. Le second
|
||||
passage conserve les hashes des fichiers gérés. Un seul JAR Sanctuary alpha.12
|
||||
est actif ; les **466 fichiers personnels et réglages suivis** conservent
|
||||
leurs hashes, y compris les sauvegardes, mods tiers et configurations. Aucun
|
||||
monde n’a été ouvert, converti ou régénéré par cette livraison.
|
||||
|
||||
La sauvegarde ciblée est dans `sanctuary-backups/before-0.1.0-alpha.12/` de
|
||||
l’instance existante. Les reçus locaux ignorés sont
|
||||
`build/alpha12-isolated-validation.json` et `build/alpha12-prism-validation.json`.
|
||||
Le [guide](alpha12-laboratory.md) décrit le nouveau preset **Sanctuary — Laboratoire** :
|
||||
créer un nouveau monde de ce type pour essayer les expansions. Les anciennes
|
||||
parties et les presets Petit/Grand gardent leur génération.
|
||||
|
||||
## Livrer une mise à jour
|
||||
|
||||
Livraison précédente, vérifiée le 9 septembre 2026 :
|
||||
[Sanctuary 0.1.0-alpha.11](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.11),
|
||||
depuis le commit source `f30a350`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
Prism sont publiés et leurs téléchargements publics vérifiés. Le canal stable
|
||||
pointait alors sur cette version. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
f075ad8a0b97cd53e4891273adbee5f79f04ab3984442950d5d85f58d2ded589
|
||||
```
|
||||
|
||||
Le véritable installateur packwiz a réussi deux fois dans un dossier de
|
||||
développement neuf, puis deux fois dans l’instance Sanctuary Beta existante.
|
||||
Le second passage laisse les fichiers gérés identiques. Un seul JAR Sanctuary
|
||||
alpha.11 est actif ; les 364 fichiers personnels et réglages suivis conservent
|
||||
leurs hashes, y compris les fichiers des mods tiers et leurs configurations.
|
||||
L’ancien JAR et les manifestes locaux sont sauvegardés hors de `mods/`, dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.11/` de cette même instance.
|
||||
Aucun monde existant n’a été ouvert, converti ou régénéré par la livraison.
|
||||
Les reçus locaux ignorés sont `build/alpha11-isolated-validation.json` et
|
||||
`build/alpha11-prism-validation.json` ; les listes de hashes avant et après
|
||||
figurent dans le dossier de sauvegarde. Les preuves du build, des deux tailles
|
||||
et de performance figurent dans [Validation](testing.md).
|
||||
|
||||
Après les vérifications, le commit et le push de la branche du ticket, le script
|
||||
de publication réalise les étapes de release et de canal ci-dessous :
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.12.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
`origin` ; il exige un arbre propre, un HEAD déjà poussé et un JAR dont la version
|
||||
correspond aux sources. Il utilise les identifiants du gestionnaire Git en
|
||||
mémoire, refuse de remplacer une release ou un artefact différent portant la
|
||||
même version, puis vérifie les téléchargements publics et les métadonnées du
|
||||
canal. Il préserve l'historique de `packwiz` et n'utilise pas de push forcé. Il
|
||||
ne lance pas les tests et ne modifie aucune instance de jeu : les vérifications
|
||||
de livraison restent à exécuter avant cette commande.
|
||||
|
||||
La procédure complète, également utilisable manuellement :
|
||||
|
||||
1. Modifier ensemble les versions de `gradle.properties` et `packwiz/pack.toml`.
|
||||
2. Exécuter `./gradlew check build assemblePack` et les vérifications du ticket.
|
||||
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.12/sanctuary-0.1.0-alpha.12.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
commit et joindre le JAR construit, sans remplacer les artefacts d'une release
|
||||
antérieure. Vérifier que le téléchargement public possède le SHA-256 indiqué
|
||||
dans `build/packwiz-release/mods/sanctuary.pw.toml`.
|
||||
5. Publier le contenu de `build/packwiz-release/` à la racine de la branche
|
||||
`packwiz` dans un seul commit, puis vérifier les fichiers et hashes depuis
|
||||
l'URL publique. Préserver l'historique de ce canal et ne jamais y copier le
|
||||
dossier `build/packwiz/`, qui contient le JAR de développement.
|
||||
6. Tester une installation puis une seconde synchronisation avec le véritable
|
||||
`packwiz-installer` dans un dossier de développement neuf. Le second passage
|
||||
doit laisser le pack à jour sans dupliquer Sanctuary.
|
||||
|
||||
`mods/sanctuary.pw.toml` conserve le même chemin entre versions ; seul son nom de
|
||||
JAR, son URL et son hash changent. Packwiz peut ainsi retirer l'ancien JAR qu'il
|
||||
gère quand il installe le nouveau. Une première installation depuis un `.mrpack`
|
||||
n'a pas encore ce suivi : voir la procédure ci-dessous.
|
||||
|
||||
Un ZIP d'amorçage Prism se prépare avec :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Il contient les versions exactes Minecraft/Fabric, la commande de lancement et
|
||||
`packwiz-installer-bootstrap` v0.0.3. Le script vérifie le SHA-256 du bootstrap
|
||||
officiel avant d'écrire `build/Sanctuary-Prism-auto-update.zip`. Ce ZIP est réservé
|
||||
aux nouvelles installations ; les instances déjà branchées sur packwiz suivent
|
||||
le canal sans être réimportées.
|
||||
|
||||
## Raccorder l'instance Prism existante
|
||||
|
||||
Fermer Minecraft avant de modifier ses mods. Fermer également Prism avant de
|
||||
modifier directement `instance.cfg`, pour éviter que son état en mémoire écrase
|
||||
les changements. Dans l'instance Sanctuary Beta :
|
||||
|
||||
1. Sauvegarder `instance.cfg` et les anciens JAR Sanctuary dans un dossier de
|
||||
sauvegarde hors de `mods/`. Vérifier que l'instance utilise Minecraft
|
||||
`26.3-pre-2` et Fabric `0.19.5` ; ne pas convertir une instance 26.2.
|
||||
2. Mettre le bootstrap issu du ZIP dans son dossier `minecraft/` (ou `.minecraft/`
|
||||
selon l'installation).
|
||||
3. Dans **Modifier → Paramètres → Commandes personnalisées**, activer les
|
||||
commandes et renseigner la commande avant lancement ci-dessous.
|
||||
4. Pour une instance issue d'un ancien `.mrpack`, déplacer seulement l'ancien
|
||||
JAR Sanctuary non géré dans la sauvegarde préparée, puis faire la première
|
||||
synchronisation packwiz. Conserver les autres mods et données personnelles.
|
||||
Si la synchronisation échoue, remettre ce JAR et les réglages sauvegardés.
|
||||
5. Vérifier qu'il reste un seul mod Sanctuary actif et que `packwiz.json` est
|
||||
présent dans le dossier Minecraft. Les prochaines mises à jour sont suivies
|
||||
par ce manifeste local : ne pas le supprimer entre les versions.
|
||||
|
||||
Commande à coller dans l'interface Prism :
|
||||
|
||||
```text
|
||||
"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" --bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" --pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Une modification directe de `instance.cfg` doit échapper les guillemets au format
|
||||
Qt INI (`\"`) et activer `OverrideCommands=true`. Ces clés et
|
||||
`PreLaunchCommand` doivent être dans la section **`[General]`**, jamais ajoutées
|
||||
après la section `[UI]`. Le ZIP préparé le fait déjà. Rouvrir les commandes
|
||||
personnalisées dans Prism pour vérifier que la commande est effectivement lue.
|
||||
Une instance importée d'un `.mrpack` peut être marquée `ManagedPack=true` :
|
||||
dès que packwiz en assure les mises à jour, utiliser `ManagedPack=false` pour
|
||||
éviter deux mécanismes de gestion sur le même pack.
|
||||
|
||||
## Données locales et mondes
|
||||
|
||||
Le pack livré possède uniquement les fichiers déclarés dans son index. Il ne
|
||||
déclare ni `saves/`, ni `options.txt`, `servers.dat`, `screenshots/`, ni les packs
|
||||
graphiques personnels. L'installation de packwiz et une mise à jour des mods
|
||||
n'autorisent pas à supprimer ou régénérer un monde.
|
||||
|
||||
La hauteur de 384 blocs, le dessous sculpté et le ciel de l'alpha.3 se testent
|
||||
dans un **nouveau monde** avec le preset **Sanctuary**. Les anciens mondes gardent
|
||||
leurs identifiants de génération et leurs hauteurs. L’hydrologie de l’alpha.4
|
||||
nécessite elle aussi un nouveau monde Sanctuary ; la mise à jour de l’instance
|
||||
n’ajoute pas d’eau aux sauvegardes existantes. Les nouvelles plages, cascades,
|
||||
biomes et petits filons de l’alpha.5 utilisent à leur tour un identifiant
|
||||
séparé : créer un nouveau monde Sanctuary pour les découvrir. Les détails des versions de génération sont dans
|
||||
[Génération](worldgen.md). Les changements de version Minecraft ou de format
|
||||
de sauvegarde demandent leur propre ticket et contrat de migration.
|
||||
|
||||
L’alpha.6 utilise à son tour `sanctuary:sanctuary_layered` pour ses biomes par
|
||||
altitude et sa nouvelle hydrologie. Créer un nouveau monde Sanctuary ; les
|
||||
sauvegardes alpha.5 conservent notamment leur palette avec Dappled Forest.
|
||||
|
||||
L’alpha.7 sélectionne `sanctuary:sanctuary_woodland` pour ses forêts, ses
|
||||
roches variées et ses minerais affleurants. Les mondes alpha.6 gardent
|
||||
`sanctuary:sanctuary_layered` ; aucune régénération de chunks n’est effectuée.
|
||||
|
||||
L’alpha.8 sélectionne `sanctuary:sanctuary_rift` pour les failles, les grandes
|
||||
rivières, les champignons géants et la géologie des profondeurs. Créer un
|
||||
nouveau monde Sanctuary pour les découvrir. La même instance Prism reçoit
|
||||
le nouveau JAR par le canal stable ; ses sauvegardes et réglages sont conservés.
|
||||
Les mondes alpha.7 et antérieurs gardent leurs paramètres de génération.
|
||||
|
||||
L’alpha.9 utilise `sanctuary:sanctuary_cavern` pour les cavités luxuriantes,
|
||||
les géodes, les geysers et les terrasses d’eau. Un **nouveau monde Sanctuary**
|
||||
est nécessaire pour essayer cet incrément. Les mondes
|
||||
alpha.8 conservent `sanctuary:sanctuary_rift` ; leur contenu n’est pas retouché
|
||||
par une mise à jour de la même instance Prism. Les anciennes sauvegardes et
|
||||
les réglages ont été conservés lors des deux synchronisations de livraison.
|
||||
|
||||
L’alpha.10 propose des choix de taille pour 5, 20 et 100 joueurs, avec
|
||||
20 par défaut. Ils s’appliquent à un **nouveau monde**. Les profils sauvegardés `sanctuary:population_5`,
|
||||
`sanctuary:population_20` et `sanctuary:population_100` sont séparés des
|
||||
anciens réglages, notamment `sanctuary:sanctuary_cavern` de l’alpha.9.
|
||||
La mise à jour de la même instance Prism n’agrandit aucune île
|
||||
existante, ne modifie pas les paramètres de génération enregistrés et ne
|
||||
nécessite pas de réimporter le ZIP d’amorçage. Les fichiers de sauvegarde,
|
||||
captures, packs graphiques et réglages personnels suivis ont conservé leurs
|
||||
hashes lors des deux synchronisations de livraison.
|
||||
|
||||
L’alpha.11 propose deux choix : **Petit** et **Grand**, Grand par défaut.
|
||||
Le preset `sanctuary:sanctuary` crée un nouveau monde `population_10` ;
|
||||
Petit garde `sanctuary:sanctuary_5` / `population_5`. Les anciens mondes
|
||||
20/100 restent chargeables à leur taille enregistrée, alors que leurs formats
|
||||
sont cachés de la création. Les formats de sauvegarde sont inchangés.
|
||||
|
||||
L’optimisation conserve les résultats pour un même réglage. La livraison a
|
||||
préservé les fichiers personnels suivis, les mods tiers et leurs configurations.
|
||||
L’instance n’a pas été réimportée et aucun mod tiers n’a été retiré. Les
|
||||
sauvegardes personnelles sont restées fermées pendant les essais et la synchronisation.
|
||||
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
@@ -1,191 +0,0 @@
|
||||
# Structures contemporaines et anciennes circulations de Sanctuary
|
||||
|
||||
Audit de conception du 9 septembre 2026, après l’essai de l’alpha.17.
|
||||
Ticket WG-21 ; branche `codex/structures-audit`.
|
||||
**Propositions à discuter : aucune nouvelle génération ni règle de progression
|
||||
n’est implémentée par ce document.** Le pack reste en alpha.17.
|
||||
|
||||
## Ce que produit actuellement le mod
|
||||
|
||||
L’alpha.17 recherche au maximum trois petits ensembles, un par famille. Chaque
|
||||
ensemble rassemble trois à cinq pièces autour d’un accès de surface, avec des
|
||||
salles généralement de 9 × 9 blocs. Leur position est cherchée indépendamment.
|
||||
Les rails sont des tronçons intérieurs de la galerie minière. Les rues et ponts
|
||||
de Lost City desservent son quartier compact. Aucun itinéraire ne relie ces
|
||||
ensembles à l’échelle de l’île : le manque d’axes perçu en jeu correspond donc
|
||||
au fonctionnement actuel.
|
||||
|
||||
Les ateliers possèdent des outils et équipements vanilla utilisables. Les
|
||||
coffres emploient les tables de maison des plaines et de cartographe ; le mod
|
||||
n’a pas encore de distribution de trésors conçue pour chaque lieu secret.
|
||||
La mailbox, les deposit boxes, la caméra, la machine d’expansion et les
|
||||
récompenses personnelles restent des systèmes futurs.
|
||||
|
||||
Sources locales : [planificateur17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsPlanner17.java),
|
||||
[rendu17](../mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/ruins/RuinsRenderer17.java),
|
||||
[preuves alpha.17](testing-alpha17.md), [vision](vision.md).
|
||||
|
||||
## Ligne de conception retenue après cet audit
|
||||
|
||||
**Minecraft comme archéologie d’un autre Minecraft.** Steve, Ari, Sunny, Kai,
|
||||
Zuri, Alex, Mefe, Makena, Noor sont les personnages disparus de la partie. Ils
|
||||
ont découvert quelque chose de révolutionnaire et ont continué parce que ça
|
||||
marchait, jusqu’au moment où ça n’a plus marché.
|
||||
|
||||
> Ils n’ont pas ouvert quelque chose qu’ils n’auraient pas dû ouvrir. Ils ont
|
||||
> construit quelque chose qu’ils ne savaient plus refermer.
|
||||
|
||||
Chaque ruine doit conserver la trace d’une ancienne solution : une infrastructure
|
||||
dont l’auteur a progressivement disparu derrière son fonctionnement. Ce canon
|
||||
ne définit pas la découverte, les rôles individuels ou une catastrophe confirmée.
|
||||
Il guide les prochaines structures ; il n’ajoute aucun personnage, récit jouable
|
||||
ou système de progression aux versions déjà livrées. L’état alpha.17 décrit
|
||||
ci-dessus reste le constat historique de cet audit.
|
||||
|
||||
Pour valider la conception d’un lieu, relever concrètement :
|
||||
|
||||
- **Un usage ancien lisible** dans les pièces, les outils et les circulations.
|
||||
- **Une implantation fonctionnelle** expliquée par le terrain, une ressource
|
||||
ou une destination réellement présente.
|
||||
- **Des traces d’entretien, d’extension ou de chantier** qui montrent comment
|
||||
l’installation a été utilisée et adaptée.
|
||||
- **Un lien futur à la progression**, explicitement décrit comme une intention
|
||||
tant que son système et ses interactions ne sont pas implémentés.
|
||||
|
||||
## Faire comprendre les usages avant d’introduire leurs systèmes
|
||||
|
||||
Chaque lieu devrait répondre à trois questions : pourquoi était-il construit
|
||||
ici, par où arrivaient ses occupants ou ses marchandises, et que peut en faire
|
||||
un joueur aujourd’hui ? Une plateforme de chargement, un vestiaire, des rayonnages
|
||||
et une sortie de service racontent davantage son usage qu’une pièce portant un nom.
|
||||
|
||||
La chaîne proposée pour l’expansion est : **observer → préparer une expédition
|
||||
→ rassembler des ressources → ouvrir un continent → construire ses liaisons**.
|
||||
Les lieux rendent ces activités compréhensibles. Ils n’imposent pas encore une
|
||||
campagne, un coût ou un ordre obligatoire de déblocage.
|
||||
|
||||
La poste rend lisible la différence entre recevoir personnellement un colis et
|
||||
contribuer à un stock collectif dans le dépôt. La salle d’expansion déjà présente
|
||||
peut devenir le point où ces efforts collectifs prennent sens. Une future
|
||||
mailbox ne doit pas être confondue avec un coffre ordinaire décoré en alpha.
|
||||
Ces bâtiments pourraient présenter les services en bêta sans obliger les joueurs
|
||||
à revenir au même guichet pour chaque action ; les règles d’usage restent ouvertes.
|
||||
|
||||
## Catalogue proposé
|
||||
|
||||
Les objets cités sont des **idées de butin**, pas des ajouts de cette livraison.
|
||||
Les variantes d’une même famille peuvent se remplacer selon la graine et le
|
||||
relief ; ce catalogue ne demande pas de tout placer sur chaque île.
|
||||
|
||||
| Lieu | Forme, pièces et implantation | Découverte en alpha à concevoir | Lien possible avec la bêta |
|
||||
| --- | --- | --- | --- |
|
||||
| **Gare de marchandises** | Quai bas, petite halle, balance, guichet, poste d’aiguillage et voie de garage ; à la jonction ville/industrie. | Comprendre la ligne abandonnée, trouver un wagonnet et quelques rails dans la réserve. | Transport des productions vers le dépôt collectif ; réparations faites par les joueurs. |
|
||||
| **Centre postal et atelier de tri** | Hall public, comptoir, casiers individuels, salle de tri, quai arrière, bureau du facteur. | Suivre physiquement le passage d’un colis de l’entrée à son casier ; boussole dans le bureau fermé. | Découverte de la mailbox personnelle et de la livraison des achats. |
|
||||
| **Halle du dépôt communal** | Grande porte, travées numérotées, aire de déchargement, mezzanine d’inventaire et atelier annexe. | Stock modeste, outils de manutention suggérés par le décor, chantier de rayonnages interrompu. | Contributions via les deposit boxes, stocks collectifs et préparation d’expansions. |
|
||||
| **Bureau des expéditions et cartothèque** | Salle de préparation, table de cartographie, vestiaire, réserves de voyage, balcon orienté vers le vide. | Boussole, papier et cartes vierges ; comparer les directions depuis le balcon. | Relevés de ressources, choix de climat et préparation des départs. Les cartes préremplies restent un travail distinct. |
|
||||
| **Observatoire et station météo** | Petite tour sur une crête, plateforme dégagée, salle des instruments, bureau abrité. | Une longue-vue vanilla dans une armoire repérable ; observation du ciel et du relief. | Constellations, signatures climatiques et futur instrument d’observation éventuel. |
|
||||
| **Dépôt de maintenance ferroviaire** | Hangar court avec fosse d’entretien, établis, pièces détachées, vestiaire et établi personnel caché. | Réparer des tronçons, récupérer un outil utile ; retrouver le travail interrompu d’un ancien occupant. | Production, maintenance et futures machines. |
|
||||
| **Station de pompage et château d’eau** | Prise d’eau, bassin, galerie de vannes, passerelles, réservoir et exutoire ; implantation dictée par l’eau et les altitudes. | Traverser les bassins, rejoindre un local sec par une coursive ; seau ou canne à pêche. | Eau pour les ateliers et cultures. Aucun système de pompage actif n’est supposé déjà disponible. |
|
||||
| **Sous-station et galerie de maintenance** | Cour technique, salle de distribution, chemins de câbles, tranchée laissée ouverte et pièce de service. | Horloge dans le poste de garde, redstone modeste dans l’atelier ; repérer les départs de galeries. | Préparer visuellement les infrastructures à réactiver. Ni réacteur ni alimentation obligatoire de l’expansion décidés ici. |
|
||||
| **Serre municipale et graineterie** | Serre partiellement effondrée, tables de rempotage, irrigation, remise et chambre de conservation. | Graines vanilla adaptées au lieu, pot de fleurs, outils de culture. | Récoltes localisées et cuisine d’**It’s Alive !** ; pas de plantes futures distribuées en alpha. |
|
||||
| **Quai d’expédition au bord du vide** | Plateforme ancrée, hangar, rambarde, grue inachevée et petite salle d’attente. | Point de vue et lieu de départ évident ; accès depuis la ville ou le dépôt. | Bateaux volants, Happy Ghast, biplans, ziplines ou ponts construits par les joueurs. Aucune île future présumée en face. |
|
||||
| **Relais radio et cabane du gardien** | Antenne sobre sur un promontoire, escalier d’entretien, logement minuscule et réserve sous le plancher. | Repère visible de loin ; boussole ou horloge, passage discret vers un atelier. | Réseau de services et rendez-vous collectifs, si ce rôle est retenu. |
|
||||
| **Club photo et laboratoire caché** | Petite boutique ou salle associative, réserve, chambre noire et séchoir ; accès secondaire depuis une cour ou un hôtel. | Un atelier secret identifiable, avec une récompense vanilla provisoire. | Lieu naturel pour découvrir une **caméra en bêta** ; aucune caméra factice ou promesse de butin inexistant en alpha. |
|
||||
| **Vestiaires et salles de service impossibles** | Derrière les grandes piscines : vestiaires, laverie, couloir de livraison, puis vaste salle carrelée hors proportion. | Étrangeté progressive, passages secondaires, récompense au bout d’un détour lisible. | Donjons physiques et secrets ; distincts de la dimension Backrooms des objets perdus. |
|
||||
| **Refuge de chantier dans un ouvrage inachevé** | Cabane dans une pile de viaduc, atelier suspendu avec ancrages, escalier arrêté à une plateforme et réserves laissées sur place. | Un lit, un outil et une installation récupérable ; sentir qu’un autre joueur s’était établi ici. | Réappropriation par les joueurs, bases individuelles et chemins à terminer. |
|
||||
|
||||
## Des axes à l’échelle de l’île
|
||||
|
||||
Une **ancienne ligne de fret**, quelques embranchements et un chemin de service
|
||||
peuvent suffire à donner une organisation au territoire. Son tracé suit la pente,
|
||||
entre brièvement dans la roche et franchit les ravins avec des ouvrages ponctuels.
|
||||
La majorité de l’île reste naturelle. Une longue voie ne signifie pas une grande
|
||||
gare ni une grande ville supplémentaire.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
M[Mine] --- A[Atelier et maintenance]
|
||||
A --- G[Gare de marchandises]
|
||||
G --- D[Dépôt communal]
|
||||
G --- P[Centre postal]
|
||||
D --- Q[Quai d'expédition]
|
||||
P --- C[Cartothèque]
|
||||
C --- O[Observatoire]
|
||||
D -. passage technique .- E[Salle d'expansion existante]
|
||||
E -. galerie de service .- H[Pompage et égouts]
|
||||
H -. accès secondaire .- S[Salles étranges]
|
||||
```
|
||||
|
||||
Ce schéma décrit les relations proposées, pas un plan rigide à reproduire ni
|
||||
des liaisons toutes ferroviaires. Mine, atelier, gare et dépôt partagent la ligne
|
||||
de fret ; les autres raccords peuvent être des rues, sentiers, escaliers ou
|
||||
galeries. Les réseaux techniques et les lieux étranges gardent leurs itinéraires
|
||||
propres, avec seulement quelques intersections utiles.
|
||||
|
||||
Pour une rupture crédible, il faut concevoir **le trajet complet entre deux
|
||||
destinations présentes**, puis son état d’abandon :
|
||||
|
||||
- **Rails arrachés** : le ballast et les traverses montrent le chemin à réparer.
|
||||
- **Petit pont effondré** : les culées et une pile conservée indiquent la portée
|
||||
manquante ; un détour permet de rejoindre l’autre côté.
|
||||
- **Tunnel éboulé** : les deux portails suivent le même axe ; une galerie de
|
||||
maintenance donne un autre parcours lorsque le relief le permet.
|
||||
- **Travaux jamais terminés** : terrassement, échafaudage, matériaux et butoir
|
||||
forment une fin intentionnelle. Un chantier peut réellement s’arrêter là.
|
||||
- **Voie envahie** : végétation et débris alternent avec des sections utilisables.
|
||||
|
||||
Les coupures ne doivent pas reproduire l’aspect d’un raccord de chunks cassé.
|
||||
Les rails qui subsistent ont un support ; les tronçons réparables suivent des
|
||||
pentes et courbes praticables en wagonnet. Un souterrain conserve des appuis et
|
||||
un passage pour le joueur. Les raccords impossibles sont abandonnés proprement,
|
||||
sans publier une porte donnant dans la roche ni annoncer une gare inexistante.
|
||||
|
||||
Une liaison vers un nouveau continent reste à construire après son ouverture.
|
||||
Le quai exprime la possibilité de partir ; il ne prédit pas un terrain encore
|
||||
absent, sa position exacte ou la route que choisiront les joueurs.
|
||||
|
||||
## Récompenser la découverte d’un lieu secret
|
||||
|
||||
Un secret devrait offrir **un objet principal cohérent avec son ancien usage** :
|
||||
longue-vue dans l’observatoire, boussole dans la cartothèque, horloge dans le poste
|
||||
d’aiguillage, outil dans l’atelier. Quelques fournitures accompagnent cet objet.
|
||||
Le plaisir vient de trouver un équipement utile tôt et de comprendre son origine ;
|
||||
un objet vanilla fabriquable ne devient pas artificiellement une relique unique.
|
||||
|
||||
L’accès se lit dans le décor : fenêtre donnant sur une pièce sans porte visible,
|
||||
escalier derrière une étagère déplacée, trappe de maintenance, conduit débouchant
|
||||
sur un établi éclairé. La pièce peut être cachée sans exiger de casser tous les
|
||||
murs au hasard. Des variations portent sur le plan, l’accès et l’usage, pas
|
||||
seulement sur la couleur d’une salle identique.
|
||||
|
||||
Le bâti est lui-même une récompense récupérable. Les rails, équipements d’atelier,
|
||||
blocs de fer et autres matériaux du décor doivent compter dans l’inventaire
|
||||
des ressources réellement générées ; une infrastructure trop généreuse pourrait
|
||||
remplacer tout le minage du début de partie. Cela se mesure après génération.
|
||||
|
||||
Pour un premier essai de butin en alpha, les coffres pourraient rester des
|
||||
conteneurs vanilla partagés, sans renouvellement automatique. La coexistence
|
||||
des **reliques collectives uniques** et des **récompenses personnelles** nécessite
|
||||
le contrat de bêta. L’accès à la mailbox ou à l’expansion ne devrait pas dépendre
|
||||
d’un objet ordinaire que le premier explorateur peut emporter et perdre.
|
||||
|
||||
## Ordre de travail proposé
|
||||
|
||||
1. Relier une destination existante et une nouvelle destination avec un axe
|
||||
visible à l’échelle de l’île : atelier/mines, gare et dépôt. Vérifier les
|
||||
trajets, les supports et quelques ruptures réparables en jeu.
|
||||
2. Donner à la ville une poste et une cartothèque ; placer un observatoire dans
|
||||
le relief, avec des accès logiques et un premier atelier secret.
|
||||
3. Raccorder ponctuellement les galeries techniques aux égouts, piscines et
|
||||
salles étranges existantes. Garder des secteurs indépendants et des accès
|
||||
secondaires plutôt que transformer tous les sous-sols en un unique donjon.
|
||||
4. Définir puis essayer les petites tables de butin par usage avant d’introduire
|
||||
les objets et règles de la bêta.
|
||||
|
||||
Avant d’écrire ce générateur, valider la place de la ligne de fret, les premiers
|
||||
lieux retenus et le rôle des ruptures : réparations libres, détours ou chantiers
|
||||
à prolonger. Les essais doivent distinguer un axe planifié, ses tronçons réellement
|
||||
générés, ceux qui sont praticables et ceux qui sont volontairement interrompus.
|
||||
La recherche des raccords doit rester bornée, sans explorer ou charger toute
|
||||
l’île pour chercher un passage. Les mondes publiés gardent leur génération ;
|
||||
aucune transformation d’une sauvegarde existante n’est autorisée par cet audit.
|
||||
@@ -1,192 +0,0 @@
|
||||
# Sanctuary — définir les lieux avant de reprendre la génération
|
||||
|
||||
**Document de travail, en discussion.** Ticket WG-26, branche
|
||||
`codex/conception-territoire`. Ce document prépare la prochaine passe ; il ne
|
||||
décrit pas une nouvelle version livrée. L’alpha.22 reste la version publiée.
|
||||
|
||||
Le périmètre est l’organisation de Sanctuary Island, la sélection des vestiges,
|
||||
leurs usages, leurs relations et les besoins des futurs ordinateurs qui
|
||||
influencent leurs espaces. Les règles détaillées des autres systèmes de la
|
||||
bêta restent dans leurs futurs chantiers.
|
||||
|
||||
## Décisions retenues dans la discussion
|
||||
|
||||
| Sujet | Décision |
|
||||
| --- | --- |
|
||||
| Arrivée | Commencer dans un endroit naturel et découvrir les premières installations en explorant. |
|
||||
| Présence ancienne | Retenir quelques lieux isolés : observatoire, salle d’expansion et atelier caché, avec la grande traversée souterraine. Renforcer leur importance et leur mystère. |
|
||||
| Villes et regroupements | Garder villes, clusters, satellites et autres bâtiments en réserve. La proposition d’un noyau gare–poste–dépôt n’est pas la sélection actuelle. |
|
||||
| Réappropriation | Les fonctions des ruines sont reproductibles dans les installations des joueurs, avec quelques lieux collectifs uniques encore à définir. |
|
||||
| Expansion | La grande salle ancienne est rééquipable. Les joueurs peuvent construire une autre installation d’expansion ailleurs ; la salle historique n’est pas un monopole géographique. |
|
||||
| Donjon majeur | Ajouter au moins un donjon difficile, essentiel à la progression, avec un boss et un objet de quête. Son objectif précis, son boss et son fonctionnement restent à définir. D’autres tunnels sont souhaités ; leur organisation reste ouverte. |
|
||||
| Architecture | Préférer les blocs de verre pour les grandes baies. Conserver les matériaux sobres appréciés ; rendre les usages lisibles dans les volumes et leurs abords. |
|
||||
| Récit | Minecraft comme archéologie d’un autre Minecraft : chaque ruine témoigne d’une ancienne solution, d’installations utilisées et de chantiers interrompus. |
|
||||
| Méthode | Définir ensemble avant de reprendre le code. Aucun nouveau binaire, aucune modification de monde ou mise à jour d’instance dans ce chantier documentaire. |
|
||||
|
||||
La demande antérieure de conserver **la grande traversée souterraine et la salle
|
||||
d’expansion indépendamment d’une ville** reste une contrainte de travail. Leur
|
||||
place exacte dans cette sélection restreinte doit être dessinée ; on ne déduit
|
||||
pas du retrait de la ville la suppression de ses infrastructures souterraines.
|
||||
|
||||
La nature de la découverte des anciens joueurs, leurs rôles et la cause précise
|
||||
de leur disparition restent ouverts. Les neuf noms et le canon sont conservés
|
||||
dans [la vision](vision.md#minecraft-comme-archéologie-dun-autre-minecraft).
|
||||
|
||||
## Ce que change cette direction
|
||||
|
||||
Le test alpha.22 Moyen, graine 0, conserve quatorze infrastructures alors que la
|
||||
ville est refusée. Cela explique qu’un placement plus exigeant puisse encore
|
||||
donner une impression d’île très occupée. La prochaine conception doit choisir
|
||||
une petite sélection avant de chercher ses emplacements : le catalogue entier
|
||||
n’est pas une liste à remplir à chaque génération.
|
||||
|
||||
Le projet de regroupements reste disponible pour la suite. La sélection actuelle
|
||||
privilégie les découvertes isolées et l’espace laissé aux nouvelles constructions.
|
||||
Les bâtiments mis en réserve restent documentés et réutilisables ; cette mise
|
||||
en réserve ne signifie pas effacer des structures dans des sauvegardes existantes.
|
||||
|
||||
## Sélection retenue et variantes en réserve
|
||||
|
||||
**Sélection validée :** observatoire, salle d’expansion et atelier caché, avec la
|
||||
grande traversée souterraine. Un donjon difficile avec boss et objet de quête
|
||||
s’ajoute à cette sélection. Les plans et la fréquence des trois lieux restent
|
||||
à définir. Excavation et piscine sont des variantes en réserve, sans ajout
|
||||
automatique à la sélection.
|
||||
|
||||
| Lieu ou variante | Ancienne solution et implantation | Utilité à définir pour l’exploration | Prolongement possible en bêta |
|
||||
| --- | --- | --- | --- |
|
||||
| Observatoire | Observer depuis une hauteur avec un ciel dégagé ; instruments et traces de relevés. | Panorama, orientation, emplacement d’observation et objet vanilla cohérent. | Comprendre les étoiles et les constellations, que les joueurs pourront observer et tracer depuis leurs propres lieux. |
|
||||
| Salle d’expansion | Volume souterrain ayant accueilli une installation collective ; accès et raccordements à comprendre. | Découverte majeure et espace rééquipable. Son état initial et les éléments récupérables restent à choisir. | Assembler une installation d’expansion ici ou ailleurs. L’architecture exacte des machines reste à définir. |
|
||||
| Atelier caché | Petit espace personnel aménagé derrière une entrée secondaire ou dans un ouvrage. | Abri, outillage ou équipement utile ; indices d’un travail interrompu. Contenu exact à choisir. | Modèle d’une installation reproductible ; aucune caméra ou machine encore absente promise dans les coffres. |
|
||||
| Donjon majeur | Ancienne fonction, implantation et cause de danger à concevoir ; le lieu doit conserver la trace d’une ancienne solution. | Objectif de conception : une exploration difficile, une rencontre avec un boss et un objet de quête nécessaire à la progression. Aucune rencontre n’est implémentée par ce document. | Étape de progression à choisir ; règles collectives, répétition du combat et attribution de l’objet encore ouvertes. |
|
||||
| Excavation rare — alternative | Ancien front d’extraction, lié à une masse rocheuse et à un gisement réel. | Descendre dans des grottes contenant des ressources encore exploitables. Richesse et rareté à équilibrer. | Extraction, transport et production construits par les joueurs. |
|
||||
| Piscine ou pool rooms — alternative | Installation hydraulique avec un premier usage identifiable, puis des espaces plus mystérieux. | Exploration, circulations humides et sèches, passages vers les profondeurs. | Éventuel donjon physique ; distinct des Backrooms, espaces artificiels orphelins dont l’histoire et l’hébergement restent à concevoir selon la [cosmologie](cosmologie.md). |
|
||||
|
||||
La traversée est une infrastructure de circulation, pas un quota de bâtiments
|
||||
annexes. Ses sorties peuvent être isolées. Les égouts peuvent exister ailleurs
|
||||
que sous une ville ; leur sélection et leurs liens avec les lieux retenus restent
|
||||
à décider. Plusieurs réseaux peuvent se rejoindre ponctuellement, conformément
|
||||
à la direction déjà retenue.
|
||||
|
||||
Restent notamment en réserve : logements, hôtel, restaurant, bibliothèque,
|
||||
caserne, places et parcs urbains ; gare, maintenance, poste, dépôt, cartothèque,
|
||||
quai, relais, serre, pompage, sous-station, club photo, scierie et autres chantiers.
|
||||
Ce regroupement sert à organiser la conception, sans modifier leurs identifiants
|
||||
ou affirmer que chaque nom correspond à un type de structure distinct déjà codé.
|
||||
|
||||
## Définir le territoire
|
||||
|
||||
Les points suivants doivent devenir des règles explicites avant l’implémentation.
|
||||
Ils n’ont pas encore de valeurs chiffrées acceptées.
|
||||
|
||||
| Règle | Choix à fixer |
|
||||
| --- | --- |
|
||||
| Départ naturel | Ce que le joueur peut voir depuis son arrivée et les espaces proches réservés à sa première construction. Traiter aussi les trappes et portiques souterrains. |
|
||||
| Première découverte | Type d’indice, distance d’exploration souhaitée, visibilité d’un lieu ou de son entrée. Une vue cachée par le relief n’est pas équivalente à un simple éloignement. |
|
||||
| Nombre de lieux | Lieux présents à chaque graine, lieux variables, remplacements possibles et limites de répétition. |
|
||||
| Espacement | Distances entre destinations, secteurs naturels continus et proximité éventuelle d’entrées de sous-sols. |
|
||||
| Tailles d’île | Base actuelle : Petit 512, Moyen 724 par défaut, Grand 1024. Décider si la sélection reste la même selon la taille ou si seuls les espacements changent. Aucun agrandissement supplémentaire n’est décidé. |
|
||||
| Relations | Pour chaque chemin ou galerie, deux destinations réelles ou une extrémité de chantier lisible ; choix des connexions directes, interrompues et seulement suggérées. |
|
||||
| Refus | Règle pour un lieu facultatif sans terrain adapté ; recherche d’un autre accès ou emplacement pour une infrastructure de base. Les garanties et les budgets devront être conciliés. |
|
||||
| Ressources | Ce que la ruine offre comme butin et comme matériaux démontables ; ce que le joueur doit encore produire lui-même. |
|
||||
|
||||
## Fiche à remplir pour chaque lieu retenu
|
||||
|
||||
Une fiche complète doit permettre de dessiner et de tester le lieu sans inventer
|
||||
sa fonction pendant le codage.
|
||||
|
||||
1. **Ancien usage :** quelle solution apportait-il, à quoi et à qui servait-il ?
|
||||
2. **Implantation :** quel terrain ou quelle ressource justifie sa présence ici ?
|
||||
3. **Découverte :** premier indice, silhouette, cheminement, entrée principale et éventuel secret.
|
||||
4. **Plan :** pièces nécessaires, dimensions, cours ou abords, circulations, accès aux sous-sols.
|
||||
5. **Architecture :** palette, structure porteuse, toiture, vitrage, détails qui expliquent l’usage.
|
||||
6. **État :** parties entretenues autrefois, réparations, chantier arrêté, dommages localisés ; aucun défaut de raccord ne fait office de ruine.
|
||||
7. **Usage dans la prochaine livraison :** ce qu’un joueur peut réellement faire dès sa découverte.
|
||||
8. **Lien futur :** système bêta envisagé, connaissances transmises, éléments reproductibles et éventuelle exception collective.
|
||||
9. **Valeur :** butin, matériaux récupérables, gisement restant, répétition et effets pour plusieurs joueurs.
|
||||
10. **Admission et vérification :** placement acceptable, cas de refus, accès praticables, limites d’eau et de lave, exemples à voir en jeu.
|
||||
|
||||
## Ordinateurs et installation d’expansion
|
||||
|
||||
**Décision acquise : l’installation est reproductible ailleurs.** Cela ne décide
|
||||
pas encore son coût, ses prérequis ou les permissions pour ouvrir un continent.
|
||||
La reproduction d’une fonction n’implique pas qu’elle soit disponible dès le début.
|
||||
|
||||
L’utilisateur réfléchit aux éléments suivants : **assembleur, bloc contrôleur,
|
||||
bloc terminal, bloc de dépôt et ancre spatiale**. Ce sont des concepts à définir,
|
||||
pas cinq blocs déjà implémentés avec des rôles figés. La vision évoque aussi un
|
||||
ordinateur 8 bits programmable avec six ports de redstone ; il faut préciser
|
||||
comment ces éléments se composent.
|
||||
|
||||
Avant de fixer le plan intérieur de la salle, décider :
|
||||
|
||||
- ce que chaque élément reçoit, conserve, exécute ou affiche ;
|
||||
- ce que désigne exactement l’assembleur et comment le joueur programme ;
|
||||
- la différence entre terminal informatique et terminal de stockage en titane ;
|
||||
- la différence entre dépôt de machine, deposit box collective et mailbox personnelle ;
|
||||
- les connexions physiques et l’encombrement d’une installation complète ;
|
||||
- la fonction de l’ancre : rattachement à un lieu, orientation, destination ou autre rôle à définir ;
|
||||
- les conditions de découverte, construction et activation, dont les ressources et autorisations côté serveur ;
|
||||
- ce qu’il reste dans la salle ancienne : installation incomplète, pièces récupérables, schéma, emplacement ou traces seulement.
|
||||
|
||||
On doit pouvoir décrire le parcours « découvrir → comprendre → construire ou
|
||||
rééquiper → préparer une expansion » avant de dessiner une interface ou de fixer
|
||||
des recettes. Cette séquence est un outil de conception, pas une campagne imposée.
|
||||
|
||||
## Donjon majeur et tunnels supplémentaires
|
||||
|
||||
Le donjon doit compter dans la progression et comporter un boss ainsi qu’un objet
|
||||
de quête. **Le blocage levé par cet objet n’est pas encore choisi** : première
|
||||
expansion, étape avancée ou objectif collectif final sont des possibilités en
|
||||
discussion, pas des règles cumulatives.
|
||||
|
||||
Sa nécessité impose de concevoir sa présence et son accessibilité sur l’île.
|
||||
Un lieu indispensable ne peut pas être traité comme une décoration facultative
|
||||
dont l’absence serait sans conséquence. Les contraintes de génération devront
|
||||
être définies avec cette exigence avant son implémentation.
|
||||
|
||||
À préciser ensemble :
|
||||
|
||||
- **Fonction ancienne :** qu’avait-on résolu ou protégé dans ce lieu ? Ne pas décider par défaut que c’est le réacteur.
|
||||
- **Découverte :** indices depuis la surface, tunnels d’approche, entrée principale et éventuels raccourcis.
|
||||
- **Difficulté :** type de préparation attendu, dangers des salles et possibilités de retraite.
|
||||
- **Boss :** identité, relation au lieu, comportements reconnaissables et conditions de victoire.
|
||||
- **Objet de quête :** ce qu’il est, ce qu’il débloque et s’il est consommé, conservé ou installé.
|
||||
- **Coopération :** victoire et déblocage à l’échelle du joueur, du groupe ou du serveur ; accueil des joueurs arrivés plus tard.
|
||||
- **Persistance :** combat unique ou rejouable, partage du butin, conséquence d’un objet perdu ou d’un site endommagé.
|
||||
- **Après la victoire :** usage de la structure, exploitation éventuelle des spawners, accès aux galeries et ressources restantes.
|
||||
|
||||
La direction antérieure des donjons prévoit des reliques collectives uniques et
|
||||
des récompenses personnelles, avec des spawners potentiellement exploitables
|
||||
après exploration. Elle ne fixe pas encore l’attribution de cet objet précis.
|
||||
|
||||
Des tunnels supplémentaires peuvent accompagner cette destination. Leur nombre,
|
||||
leurs connexions et leur étendue restent ouverts : on conserve l’idée de réseaux
|
||||
parfois connectés, sans imposer une galerie continue sous toute l’île. La grande
|
||||
traversée utile aux déplacements et le parcours difficile du donjon doivent être
|
||||
distingués lors du dessin de leurs accès.
|
||||
|
||||
## Décisions ouvertes et ordre de discussion
|
||||
|
||||
| Étape | Résultat attendu | État |
|
||||
| --- | --- | --- |
|
||||
| 1. Fonctions reproductibles | Règle commune et place des exceptions collectives. | Principe retenu ; exceptions à nommer. |
|
||||
| 2. Salle d’expansion | Lieu rééquipable et installation reproductible ailleurs. | Retenu ; contenu initial à définir. |
|
||||
| 3. Sélection | Observatoire, salle d’expansion, atelier caché, grande traversée et donjon majeur. | Retenue ; tunnels supplémentaires et fréquences à définir. |
|
||||
| 4. Objet du donjon | Étape de progression débloquée, puis nature du boss et de l’objet. | Question en cours. |
|
||||
| 5. Territoire | Arrivée, découverte, quantités, espacements et différences entre les trois tailles. | À définir. |
|
||||
| 6. Fiches des lieux | Plans, utilité immédiate, indices, matériaux et état d’abandon. | À définir pour la sélection retenue. |
|
||||
| 7. Machines | Rôles et connexions nécessaires pour dimensionner la salle. | Concepts en discussion. |
|
||||
| 8. Contrat de génération | Garanties, refus, ressources, exemples visuels et critères d’essai. | À définir avant de créer les tickets de code. |
|
||||
|
||||
Le document pourra alors servir de cahier de conception validé. Chaque future
|
||||
livraison prendra un périmètre vérifiable dans ce cahier ; une discussion ou une
|
||||
fiche incomplète n’est pas une fonctionnalité implémentée.
|
||||
|
||||
## Références et constats
|
||||
|
||||
- [Vision](vision.md) : intentions de jeu et canon narratif.
|
||||
- [Audit des structures](structures-audit.md) : audit historique et catalogue de possibilités.
|
||||
- [Architecture alpha.20](architecture-alpha20.md) : formes de bâtiments construites lors de cette passe.
|
||||
- [Essais alpha.22](testing-alpha22.md) : état livré, graines et limites vérifiées.
|
||||
- Les difficultés Windows signalées restent un sujet de diagnostic distinct ; leur cause n’est pas attribuée à la préversion sans preuve.
|
||||
@@ -1,362 +0,0 @@
|
||||
# Alpha.14 — Lost City et expansions
|
||||
|
||||
**État : alpha.14 vérifiée, publiée et installée dans la même instance Prism Mac.**
|
||||
`check build assemblePack` passe sur les sources finales avec les **8 GameTests
|
||||
requis**, le 9 septembre 2026 à **13 h 57 min 48 s**. La création de quartiers,
|
||||
leur réouverture et la reprise différée du vide passent également dans leurs
|
||||
mondes de développement distincts. Le JAR, le MRpack et le ZIP Prism sont
|
||||
vérifiés. La release immuable correspond au commit source `1c5c2c2`, le canal
|
||||
packwiz au commit `dabf57e`. Deux synchronisations isolées puis deux dans la même
|
||||
instance Prism passent, avec **713 fichiers personnels et réglages suivis
|
||||
conservés**. Le [reçu de livraison](packwiz.md#alpha14-publiée--9-septembre-2026)
|
||||
consigne les artefacts, leur empreinte et la sauvegarde ciblée.
|
||||
Les résultats des alpha.12, 13 et 13.1 restent historiques. Ticket [WG-17](backlog.md).
|
||||
|
||||
Les smokes des sources finales passent sous Gradle et Java 25 :
|
||||
`LostCityRendererSmoke` pour les 17 types, dont confinement
|
||||
et accès des piscines dans quatre orientations, parcs distincts et sous-sols ;
|
||||
`ExpansionJournalSmoke`, dont l’immuabilité du jeton de reprise du vide ; et
|
||||
le contrôle de placement spatial de génération 14. `LostCityPlannerSmoke`
|
||||
passe également. Ses
|
||||
32 graines sur terrain contrôlé couvrent tous les types de lots ; un témoin
|
||||
distinct de faille couvre le pont, soit les 17 types. Le test de pente ascendante
|
||||
et descendante protège la sélection des lots contre le rejet asymétrique des
|
||||
terrains montants. Sur l’ancien planificateur compilé, ce nouveau test échouait
|
||||
avec **0 contre 9 lots** ; le planificateur corrigé satisfait l’égalité des
|
||||
deux sens et le minimum de cinq lots demandé par le test. Cette comparaison
|
||||
provient de la sortie d’exécution ; aucun journal séparé n’a été conservé.
|
||||
|
||||
`:sanctuary:expansionVoidCertificateSmoke` passe aussi sous Gradle :
|
||||
champs NBT natifs obligatoires, types stricts, refus des contenus, preuves de
|
||||
chunks absents et stabilité du certificat face aux seuls compteurs volatils.
|
||||
Ces contrôles unitaires sont complétés par les essais moteur consignés plus
|
||||
bas.
|
||||
|
||||
## Périmètre et création d’un monde
|
||||
|
||||
Minecraft **26.3-pre-2**, Fabric et Java **25**, selon les versions épinglées dans
|
||||
`gradle.properties`. La livraison cible `0.1.0-alpha.14` pour le mod et le pack.
|
||||
|
||||
Créer un **nouveau monde**, type **Sanctuary**, puis le bouton natif
|
||||
**Personnaliser** : Petit 512, Moyen 724 par défaut ou Grand 1 024 blocs de
|
||||
diamètre nominal. La hauteur reste de 384 blocs. Le codec public est
|
||||
`sanctuary:island_v14` ; les racines et les extensions de ces mondes portent la
|
||||
génération **14**. Le journal est séparé :
|
||||
`data/sanctuary-world-v14/expansions.json`, schéma 1.
|
||||
|
||||
Les mondes de générations 12 et 13 conservent leurs codecs, paramètres et
|
||||
journaux. Ils ne reçoivent pas Lost City par conversion et ne changent pas de
|
||||
générateur au chargement. Aucune sauvegarde personnelle ne doit être utilisée
|
||||
pour les essais ci-dessous.
|
||||
|
||||
## Lost City
|
||||
|
||||
Les quartiers sont composés par du code Sanctuary original. Une poche urbaine
|
||||
peut être retenue sur l’île initiale ; les extensions de profil `lost_city`
|
||||
recherchent également des emplacements. Le biome suit les parcelles acceptées
|
||||
en surface et laisse les biomes de cavités profonds. Une île peut ne fournir
|
||||
aucun site convenable : la ville et ses différents types ne sont pas garantis.
|
||||
|
||||
| Type | Aménagement distinctif |
|
||||
| --- | --- |
|
||||
| Rue | Chaussée, bordures de pierre lisse et variations de hauteur accessibles |
|
||||
| Pont | Tablier et garde-corps au-dessus d’une courte faille, appuis naturels aux deux extrémités |
|
||||
| Parc boisé | Chemins croisés, arbres et bancs |
|
||||
| Jardin | Haies basses, parterres fleuris et allées |
|
||||
| Aire de jeux | Sable, portique de balançoire, plate-forme et petit toboggan |
|
||||
| Parc mémorial | Cour, monument de pierre et bancs |
|
||||
| Place | Pavage et fontaine sèche |
|
||||
| Marché | Étals, auvents sobres et réserves |
|
||||
| Parking | Places marquées, poteaux, garage souterrain si la roche le permet, sinon en surface |
|
||||
| Excavation | Fouille ouverte, fondations inachevées, barrières et escalier |
|
||||
| Piscine abandonnée | Bassin de trois couches d’eau, marches immergées et plage minérale |
|
||||
| Immeuble d’appartements | Étages, chambres, coin cuisine et escaliers |
|
||||
| Hôtel | Réception, couloir central et chambres séparées |
|
||||
| Bibliothèque | Rayonnages, tables de lecture et pupitre |
|
||||
| Restaurant | Cuisine équipée et tables, espace de service à l’étage |
|
||||
| Caserne | Dortoirs et réserves |
|
||||
| Hall | Grande salle, estrade, rangées de sièges et balcon |
|
||||
|
||||
Pierre, pierre lisse et briques de pierre dominent ; briques fissurées ou
|
||||
moussues, andésite, vitres, détails de fer et intérieurs en chêne noir complètent
|
||||
la palette. L’usure est déterministe. Les accès et les escaliers restent
|
||||
distincts des brèches de façade et de toiture. Le butin des coffres utilise des
|
||||
tables vanilla différées ; aucune génération de spawner ou de villageois.
|
||||
|
||||
Le spawn naturel de monstres du biome et des pièces de structure est limité aux
|
||||
zombies vanilla. Lumière, difficulté, limites de population et distance aux
|
||||
joueurs continuent de s’appliquer ; une ruine n’implique pas la présence immédiate
|
||||
d’un groupe de zombies. Les classes de zombies spécifiques restent pour la bêta.
|
||||
|
||||
Le placement vérifie la roche sous toute l’emprise. Un sous-sol, une piscine ou
|
||||
une excavation exige une continuité jusqu’à huit blocs sous le plancher de
|
||||
référence. À défaut, le parking reste en surface et piscine ou excavation se
|
||||
replient en parc. Les piscines gardent un fond et des parois continus ; les
|
||||
ponts ne construisent pas de piliers jusqu’au vide. Les volumes protégés de
|
||||
l’hydrologie et les sites insuffisamment supportés sont écartés.
|
||||
|
||||
## Commandes d’essai et placement des expansions
|
||||
|
||||
Les commandes restent des outils opérateur, accessibles avec les permissions
|
||||
adéquates. Elles n’utilisent pas encore la progression ou les ressources.
|
||||
|
||||
```text
|
||||
/sanctuary expansion quick east lost_city 256 natural
|
||||
/sanctuary expansion list
|
||||
/sanctuary expansion visit island_1
|
||||
/sanctuary expansion preview quartier sanctuary northeast lost_city 128 natural
|
||||
/sanctuary expansion create quartier sanctuary northeast lost_city 128 natural
|
||||
/locate structure sanctuary:lost_city
|
||||
```
|
||||
|
||||
`island_1` est l’exemple du premier identifiant libre créé par `quick` ; utiliser
|
||||
l’identifiant réellement indiqué par la commande. `visit` attend une île prête.
|
||||
`preview` examine un emplacement sans l’activer. `resume <id>` sert à reprendre
|
||||
une réservation interrompue. `create` choisit aussi son parent explicitement.
|
||||
La recherche exacte `/locate structure sanctuary:lost_city` retrouve les
|
||||
quartiers Sanctuary admissibles ; elle peut échouer si aucun quartier ne se
|
||||
trouve dans sa portée de recherche. Un profil `lost_city` ne garantit pas un
|
||||
terrain suffisamment adapté pour accepter un quartier.
|
||||
|
||||
Les diamètres d’expansion possibles dans les mondes 14 sont **64, 128, 256,
|
||||
512 et 1 024**. Avec `quick east` ou `quick east lost_city`, le diamètre est tiré
|
||||
de manière reproductible à partir de la graine et de l’identifiant :
|
||||
|
||||
| Diamètre | Poids du tirage |
|
||||
| --- | --- |
|
||||
| 64 | 40 % |
|
||||
| 128 | 30 % |
|
||||
| 256 | 18 % |
|
||||
| 512 | 9 % |
|
||||
| 1 024 | 3 % |
|
||||
|
||||
Ces poids décrivent la distribution du tirage, pas un quota de cinq îles à
|
||||
reproduire sur chaque serveur. Une taille explicite remplace le tirage. Les
|
||||
recherches d’emplacement ne changent ni cette taille ni la graine retenue.
|
||||
Les mondes historiques gardent leurs tailles et comportements antérieurs.
|
||||
|
||||
La direction choisit un secteur de 45° autour du parent. La recherche essaie
|
||||
d’abord des positions proches, puis des couronnes plus éloignées, avec un
|
||||
nombre borné de candidats. Les centres restent alignés sur les chunks.
|
||||
Les emprises circulaires incluent le terrain, la marge de décoration et une
|
||||
garde empêchant deux îles de posséder le même chunk de coin. Le voisinage lu
|
||||
par les étapes de génération n’est pas intégralement réservé comme terrain.
|
||||
Une distance minimale entre centres reste possible comme dernier argument.
|
||||
|
||||
## Contrat de reprise différée du vide
|
||||
|
||||
Ce mécanisme concerne **uniquement les nouveaux mondes de génération 14** et
|
||||
une expansion explicitement demandée. Les générations 12/13 gardent leur refus
|
||||
des chunks déjà générés, même vides. Il ne permet pas la régénération générale
|
||||
d’une île, d’un terrain occupé ou d’une construction.
|
||||
|
||||
1. Examiner toute l’emprise d’écriture et produire, pour chaque chunk existant,
|
||||
une preuve stricte qu’il est FULL, entièrement vide et vierge. Les positions
|
||||
encore absentes sont également certifiées, y compris leurs stockages
|
||||
d’entités et de POI. La simple absence de bloc visible ne suffit pas : entités
|
||||
persistées, chargées ou en attente, POI, ticks, événements de blocs et autres
|
||||
indices de contenu font partie des refus.
|
||||
2. Lier le certificat à l’identité exacte de l’expansion et à un jeton immuable
|
||||
dans le journal. Une preuve isolée sans réservation correspondante ne donne
|
||||
aucune autorisation ; un changement de paramètres ou une preuve manquante
|
||||
doit faire échouer la reprise.
|
||||
3. Enregistrer la réservation et demander de **fermer puis rouvrir le monde**.
|
||||
L’expansion reste différée pendant la session ayant établi la preuve.
|
||||
4. Au chargement suivant, vérifier le journal, le certificat et les données
|
||||
actuelles sur toute l’emprise avant toute préparation. Une divergence bloque
|
||||
la reprise avant l’écriture des positions absentes. Seuls les chunks
|
||||
explicitement certifiés peuvent être réutilisés par cette expansion.
|
||||
|
||||
La preuve lit le format natif exact de Minecraft 26.3-pre-2 : l’état d’air sans
|
||||
propriétés s’écrit comme la chaîne `minecraft:air` dans la palette ; sa forme
|
||||
composée autorisée utilise le champ `id`. L’ancien champ `Name` ne suffit pas
|
||||
à certifier un chunk. Les types NBT, sections, palettes, heightmaps et données
|
||||
annexes restent contrôlés strictement ; une métadonnée inconnue provoque un
|
||||
refus. Seuls les compteurs temporels et caches de lumière valides sont exclus
|
||||
de l’empreinte sémantique du vide.
|
||||
|
||||
Il ne faut supprimer ni certificat, ni journal, ni dossier de région pour
|
||||
débloquer une commande. L’absence de preuve est un refus, pas une invitation à
|
||||
reconstruire l’historique. Les sauvegardes personnelles ne sont pas ouvertes
|
||||
par les tests ou par la mise à jour packwiz.
|
||||
|
||||
## Protocole et preuves attendues
|
||||
|
||||
Un seul processus Gradle ou Minecraft de développement à la fois. Les suites
|
||||
de création repartent de mondes ignorés sous `mods/sanctuary/build/run/`.
|
||||
Un scénario de réouverture doit suivre sa création sans intercaler un test qui
|
||||
effacerait ce monde de développement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:lostCityPlannerSmoke :sanctuary:lostCityRendererSmoke --console=plain
|
||||
./gradlew check build assemblePack -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryLostCityTests=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryLostCityTests=true -PsanctuaryExpansionReload=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
La suite de reprise du vide repart ensuite d’un **autre monde de développement
|
||||
neuf**. La commande de réouverture est exécutée deux fois de suite :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryVoidReuseTests=true -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryVoidReuseTests=true -PsanctuaryExpansionReload=true -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryVoidReuseTests=true -PsanctuaryExpansionReload=true -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
### Résultats moteur — graine 0
|
||||
|
||||
Le 9 septembre 2026, la création sur les sources finales passe à **13 h 55 min
|
||||
15 s** dans le processus **19155** (`build/alpha14-city-final-create.log`,
|
||||
deux tests requis réussis, commande terminée en **2 min 24 s**). La réouverture
|
||||
du même monde de développement passe à **13 h 55 min 58 s** dans un second
|
||||
processus, **19372** (`build/alpha14-city-final-reload.log`, deux tests requis
|
||||
réussis, **33 s**).
|
||||
Ces durées sont celles des commandes Gradle complètes, pas des mesures de
|
||||
chargement en jeu ou de performance multijoueur.
|
||||
|
||||
Les rapports locaux ignorés sont
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/lost-city-alpha14-seed-0.json`
|
||||
et `lost-city-alpha14-reload-seed-0.json` dans le même dossier. Leurs copies
|
||||
sont conservées dans `build/alpha14-evidence/` avant le nettoyage du monde de
|
||||
développement. Le monde racine est de diamètre **724**, graine **0**, codec
|
||||
`sanctuary:island_v14`.
|
||||
|
||||
Trois extensions de diamètre 256 ont été examinées en lecture seule :
|
||||
|
||||
| Candidat | Graine de l’extension | Quartiers admissibles |
|
||||
| --- | --- | --- |
|
||||
| `city_test` | `7890860792428309400` | 0 |
|
||||
| `city_harbor` | `-1594320136210322067` | 0 |
|
||||
| `city_gardens` | `6666275329838981856` | 1 |
|
||||
|
||||
Seule `city_gardens` a ensuite été activée, à l’est, centre **(2032, 624)**.
|
||||
Sa préparation a terminé **418 chunks FULL**. L’inspection urbaine ci-dessous
|
||||
couvre les chunks des quartiers, pas la totalité de cette préparation.
|
||||
|
||||
| Site généré | Parcelles sauvegardées | Chunks FULL inspectés | Coffres non ouverts à butin différé |
|
||||
| --- | --- | --- | --- |
|
||||
| Île initiale | 26 : appartement, hôtel, bibliothèque, marché, parc boisé, mémorial et 20 rues | 26 | 4 |
|
||||
| `city_gardens` | 12 : appartement, parc boisé et 10 rues | 13 | 1 |
|
||||
|
||||
Les deux quartiers possèdent leurs références de structure natives ; la
|
||||
recherche native retrouve respectivement **(-49, 253, 13)** et **(2012, 229,
|
||||
621)**. Les intérieurs traversables et les tables de spawn limitées aux zombies
|
||||
sont contrôlés. Cela ne garantit pas des zombies visibles de jour. La
|
||||
couverture des **17 types dans quatre orientations** provient de 68 rendus
|
||||
témoins indépendants ; elle ne signifie pas que cette graine contient tous les
|
||||
types dans ses quartiers naturels.
|
||||
|
||||
L’inspection native des bâtiments contrôle également qu’aucun minerai ne
|
||||
remplace la maçonnerie dans leurs volumes à partir du plancher. Cette preuve
|
||||
concerne les bâtiments effectivement générés et inspectés ; les minerais du
|
||||
terrain sous les fondations restent possibles.
|
||||
|
||||
Une piscine native isolée, indépendante du tirage des quartiers, conserve
|
||||
**189 cellules d’eau**, ses marches et son confinement après l’exécution d’un
|
||||
tick d’eau. La réouverture conserve les mêmes 189 cellules et les marches ;
|
||||
elle n’exécute pas un second tick témoin.
|
||||
|
||||
Dans le second processus, les **38 pièces natives**, les **5 coffres non
|
||||
ouverts**, le bloc d’or témoin à **(-61, 250, 11)** et le journal sont conservés.
|
||||
L’empreinte du journal contrôlé est
|
||||
`38e4bc1b2e493628d1e5d1775250c0bc48821bf49d025df3c37159fcff5801ba`.
|
||||
Ces essais utilisent uniquement un monde de développement ignoré, sans ouvrir
|
||||
de sauvegarde personnelle.
|
||||
|
||||
### Reprise du vide — création et deux réouvertures
|
||||
|
||||
Les trois exécutions Minecraft, sur la graine **0**, génération **14**, passent
|
||||
le 9 septembre 2026 :
|
||||
|
||||
| Étape | Journal local | Résultat et durée de la commande |
|
||||
| --- | --- | --- |
|
||||
| Preuves et réservations | `build/alpha14-void-create.log` | Deux tests requis réussis à 13 h 43 ; 41 s |
|
||||
| Première réouverture | `build/alpha14-void-reload.log` | Deux tests requis réussis à 13 h 45 ; 1 min |
|
||||
| Deuxième réouverture | `build/alpha14-void-second-reload.log` | Deux tests requis réussis à 13 h 46 ; 29 s |
|
||||
|
||||
Les rapports sont `void-reuse-before-reload.json`,
|
||||
`void-reuse-after-reload.json` et `void-reuse-second-reload.json`, sous
|
||||
`mods/sanctuary/build/run/gameTest/world/`. Ce sont des preuves locales à
|
||||
conserver avant le nettoyage du monde de développement.
|
||||
|
||||
La première expansion, `void_loaded`, couvre **77 chunks FULL vides déjà
|
||||
chargés**. La seconde, `void_saved`, couvre **un chunk FULL vide uniquement
|
||||
stocké et 76 positions absentes**. Ce dernier scénario emploie une copie NBT
|
||||
native de chunk vide, déplacée uniquement pour constituer le témoin dans ce
|
||||
monde jetable. Chaque réservation reçoit son jeton durable propre et reste
|
||||
différée, sans publication dans le générateur pendant la session initiale.
|
||||
|
||||
Quatre témoins indépendants, chacun contrôlé vide avant modification, prouvent
|
||||
le refus d’un **bloc**, d’un **coffre avec son entité de bloc**, d’une **entité
|
||||
vivante enregistrée** et d’un **tick de bloc programmé**. Une métadonnée NBT
|
||||
étrangère ajoutée après certification fait également échouer la vérification
|
||||
globale : l’empreinte SHA-256 du fichier de région et le contenu NBT modifié
|
||||
restent identiques après ce refus. Le test restaure ensuite son témoin vide
|
||||
pour poursuivre le scénario positif.
|
||||
|
||||
À la première réouverture, les deux expansions atteignent **154 chunks FULL**,
|
||||
chacun conservant le marqueur de son jeton exact, avec **128 sections non
|
||||
vides**. Un bloc d’or témoin est alors posé à **(6080, 376, 1264)**. À la
|
||||
deuxième réouverture, il est préservé : les **154 chunks FULL marqués** et
|
||||
**129 sections non vides** sont retrouvés. Ce passage contrôle donc aussi
|
||||
qu’une reprise terminée ne réinitialise pas les blocs ajoutés ensuite.
|
||||
|
||||
Une réouverture supplémentaire passe à **13 h 48**, avec deux tests requis
|
||||
réussis en **37 s** (`build/alpha14-void-final-reload.log`). Elle contrôle
|
||||
explicitement la lumière native terminée (`isLightCorrect`) sur les **154
|
||||
chunks** et **32 échantillons de biomes hors `the_void`** à l’intérieur des
|
||||
extensions. Le bloc d’or et les 129 sections non vides sont toujours conservés.
|
||||
Ce dernier rapport est copié hors du monde de test dans
|
||||
`build/alpha14-evidence/void-reuse-second-reload.json` pour survivre à son
|
||||
nettoyage. Les 32 échantillons ne constituent pas un relevé exhaustif de tous
|
||||
les biomes des extensions.
|
||||
|
||||
### Vérification finale du build et des artefacts
|
||||
|
||||
`./gradlew check build assemblePack` passe en **1 min 34 s** : **40 tâches**, dont
|
||||
33 exécutées et 7 à jour. Le journal est `build/alpha14-check-build-pack.log`.
|
||||
Les huit tests moteur requis réussissent ; le témoin hydrologique de graine 0
|
||||
contrôle **125 chunks FULL**, **5 923 blocs d’eau conservés** et **1 800 ticks de
|
||||
fluides**. La même suite vérifie la sérialisation des tailles **512 / 724 /
|
||||
1 024**, le défaut Moyen et le codec historique de génération **13**.
|
||||
|
||||
Le smoke du journal valide aussi la politique de synchronisation de dossier
|
||||
Windows par simulation, ainsi que les échecs de stockage après remplacement.
|
||||
Ce résultat ne constitue pas une exécution native sous Windows.
|
||||
|
||||
`build/verify-alpha14-artifacts.py` valide les versions alignées, l’index packwiz,
|
||||
le JAR de production sans classes de test, son inclusion dans le MRpack et le
|
||||
ZIP d’amorçage Prism. Le SHA-256 du JAR `sanctuary-0.1.0-alpha.14.jar` est :
|
||||
|
||||
```text
|
||||
1c517a13e1151501c6f4974d1bc00faedf67aaecd9bf58f34ac4a655d09dc6ef
|
||||
```
|
||||
|
||||
Ces vérifications portent sur les artefacts locaux. La publication du canal,
|
||||
les installations packwiz isolées et la synchronisation de l’instance Prism
|
||||
restent à consigner.
|
||||
|
||||
| Contrôle | Preuve attendue | État de livraison |
|
||||
| --- | --- | --- |
|
||||
| Géométrie et fondations | Déterminisme, budget de colonnes, sites protégés, certificat de roche et ponts | Smoke Gradle réussi sur les sources finales |
|
||||
| Rendu des 17 types | Quatre orientations, limites des pièces, parcours vers les étages, lits complets, accès aux sous-sols et piscine contenue | Smoke Gradle réussi sur les sources finales |
|
||||
| Placement des expansions | Cinq tailles, tirage stable, huit secteurs, séparation et garde aux coins | Smoke Gradle réussi sur les sources finales |
|
||||
| Structures dans Minecraft | Quartier initial et extension Lost City réellement générés, références natives et coffres à butin différé | Réussi, graine 0 : 38 pièces et 5 coffres |
|
||||
| Rechargement dans un second processus | Journal et structures identiques, butin non recréé, bloc témoin de joueur conservé | Réussi sur les sources finales, graine 0, processus 19372 |
|
||||
| Protection de la maçonnerie | Aucun minerai dans les volumes des bâtiments à partir de leur plancher | Réussi sur les quartiers natifs inspectés, graine 0 |
|
||||
| Eau après ticks moteur | Piscine retenue et accès après écoulement, sans fuite extérieure | Réussi sur piscine native isolée : 189 cellules et marches conservées après tick puis réouverture |
|
||||
| Certificat de vide | Schéma NBT strict, refus des contenus et preuves de chunks absents | Smoke Gradle réussi, complété par les témoins natifs de création |
|
||||
| Reprise du vide | Refus des contenus et preuves invalides ; reprise bornée après réouverture avec preuve intacte | Réussi après deux réouvertures : 154 chunks FULL marqués, bloc d’or conservé |
|
||||
| Lumière et biomes après reprise | Lumière native terminée et remplacement du biome vide à l’intérieur des extensions | Réussi à 13 h 48 : 154 chunks éclairés correctement, 32 échantillons hors `the_void`, bloc d’or conservé |
|
||||
| Sérialisation et stockage | Trois tailles, codec historique et erreurs de stockage bloquantes | Suite native réussie pour les trois tailles et le codec 13 ; politique Windows simulée réussie |
|
||||
| Hydrologie conservée | Eau retenue après écoulement moteur | Réussi : 125 chunks FULL, 5 923 blocs d’eau, 1 800 ticks de fluides |
|
||||
| Build et pack | `check build assemblePack` réussi avec versions alignées | Réussi à 13 h 57 min 48 s, huit GameTests requis, JAR/MRpack/ZIP Prism vérifiés |
|
||||
| Distribution | Artefacts immuables vérifiés, canal avancé, deux installations isolées et deux synchronisations de la même instance | Réussi : source `1c5c2c2`, canal `dabf57e`, 713 fichiers personnels et réglages suivis conservés |
|
||||
|
||||
Les reçus locaux sont `build/alpha14-isolated-validation.json` et
|
||||
`build/alpha14-prism-validation.json`. Aucun essai Windows natif, mesure de FPS ou test
|
||||
de charge multijoueur n’est attesté par cette liste. Une validation de géométrie
|
||||
en mémoire ne remplace pas la génération, les ticks et la persistance du moteur.
|
||||
|
||||
La publication suit [Distribution packwiz et Prism](packwiz.md). Elle conserve
|
||||
l’instance existante, ses sauvegardes, ses réglages et les mods tiers.
|
||||
@@ -1,182 +0,0 @@
|
||||
# Alpha.15 — intérieurs, sous-sols et ville compacte
|
||||
|
||||
**État : code, génération et distribution validés.**
|
||||
Ticket [WG-18](backlog.md). `check build assemblePack` et les huit tests moteur
|
||||
passent sur les sources finales, ainsi que la création et la réouverture
|
||||
natives de Lost City. Le reçu de publication figure dans [Distribution](packwiz.md).
|
||||
|
||||
## Contrat de génération
|
||||
|
||||
Minecraft **26.3-pre-2**, Java **25**, dépendances épinglées inchangées.
|
||||
Le mod et le pack passent à `0.1.0-alpha.15`. Un **nouveau monde Sanctuary**
|
||||
utilise `sanctuary:island_v15` et son journal propre
|
||||
`data/sanctuary-world-v15/expansions.json`.
|
||||
Le bouton natif **Personnaliser** conserve Petit 512, Moyen 724 par défaut et
|
||||
Grand 1 024. La densité du terrain, les forêts et l’hydrologie naturelle ne sont
|
||||
pas remplacées par cette passe urbaine.
|
||||
|
||||
Le JAR d’essai anticipé du 9 septembre, avant publication de l’alpha.15,
|
||||
contenait encore les fragments dispersés. Les essais de cette nouvelle
|
||||
composition se font dans un **monde neuf** ; aucune réparation ou régénération
|
||||
des mondes créés avec ce prototype n’est exécutée. Le binaire d’essai est
|
||||
conservé séparément avant synchronisation de l’instance.
|
||||
|
||||
Les mondes 12/13/14 conservent leurs codecs, paramètres, journaux, plans et
|
||||
rendus, même pour leurs chunks encore non générés. Le preset 14 reste enregistré
|
||||
mais caché dans la création. Aucun monde personnel n’est ouvert, converti ou
|
||||
régénéré. Les nouvelles pièces persistantes ont leurs propres identifiants :
|
||||
`sanctuary:lost_city_parcel_v15`, `sanctuary:lost_city_underground_v15` et
|
||||
`sanctuary:lost_city_support_v15`.
|
||||
|
||||
Le biome `sanctuary:lost_city` reste utilisé : les parcelles acceptées pilotent
|
||||
la carte des biomes urbains et les apparitions naturelles de zombies vanilla.
|
||||
Les cavités gardent leurs biomes ; un égout souterrain ne transforme pas toute
|
||||
la colonne en biome urbain. Il n’y a ni villageois générés, ni spawner, ni
|
||||
classes de zombies de la future bêta.
|
||||
|
||||
## Aménagements
|
||||
|
||||
Les appartements, hôtels, bibliothèques, restaurants, casernes et halls ont
|
||||
des usages distincts. Seuil et vestibule rejoignent un couloir, une cage
|
||||
d’escalier et des pièces séparées par de vraies portes. Sols, plafonds,
|
||||
chambres, salles d’eau, cuisines, réserves, réception, rayonnages, dortoirs,
|
||||
auditorium et galerie suivent le type du bâtiment. Les détails d’usure doivent
|
||||
préserver les passages. Le butin reste différé, avec un coffre par bâtiment.
|
||||
|
||||
Les réseaux souterrains cherchent une descente depuis la surface, des égouts,
|
||||
des passages, des garages et des pool rooms. La roche et les volumes d’eau
|
||||
naturelle décident de leur admission. Les parties exposées demandent des
|
||||
appuis courts certifiés : pylônes ou chaînes réellement ancrés. Les bassins
|
||||
construits ont un fond, des parois et un accès ; ils ne doivent pas se vider
|
||||
dans les corridors. Les modules voisins ne se superposent pas. Une grande salle sèche accessible
|
||||
prépare une future machine d’expansion ; la machine et ses cartes ne font pas
|
||||
partie de cette livraison.
|
||||
|
||||
Le retour de l’essai anticipé remplace les fragments dispersés par une grille
|
||||
compacte : lots, rues et carrefours sont jointifs, avec un réseau accessible
|
||||
depuis chaque entrée. Les routes sont dégagées sur huit blocs de hauteur ;
|
||||
les terrasses sont certifiées dans leur emprise et les ponts prolongent le
|
||||
réseau. Les sièges, charnières et appuis des lanternes sont revus. Les emplacements admissibles
|
||||
restent conditionnels ; aucun équipement n’est promis sur toute graine.
|
||||
|
||||
## Vérification reproductible
|
||||
|
||||
Utiliser seulement les mondes de développement ignorés. Exécuter les processus
|
||||
Minecraft séquentiellement et faire suivre chaque création de sa réouverture,
|
||||
avant un autre test qui nettoie ce dossier de développement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:lostCityInteriors15Smoke :sanctuary:lostCityUnderground15Smoke --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryLostCity15Tests=true -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryLostCity15Tests=true -PsanctuaryExpansionReload=true -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew check build assemblePack -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
La suite historique `-PsanctuaryLostCityTests=true` choisit explicitement la
|
||||
génération 14. Les suites de reprise certifiée du vide utilisent la génération
|
||||
15 par défaut ; la réouverture doit être exécutée deux fois après leur création.
|
||||
Les certificats sont liés à leur génération : un certificat ou journal 14 ne
|
||||
peut pas autoriser une expansion 15. Le [contrat WG-17](testing-alpha14.md)
|
||||
reste applicable dans chaque journal, avec inspection stricte avant écriture.
|
||||
|
||||
Les résultats finaux préciseront graine, version, coordonnées, pièces natives,
|
||||
eau après ticks, conservation après réouverture et durée des commandes.
|
||||
|
||||
### Compatibilité historique déjà vérifiée
|
||||
|
||||
La création de génération 14 avec le binaire alpha.15 passe le 9 septembre
|
||||
2026 (`build/alpha15-legacy14-create.log`, processus 22663, 2 min 57 s).
|
||||
Sur graine 0, racine 724 et extension `city_gardens` 256, les deux starts NBT,
|
||||
les inventaires de blocs, les coffres différés et le journal ont exactement les
|
||||
mêmes empreintes ou contenus que la livraison alpha.14. La réouverture du même
|
||||
binaire dans le processus 23083 passe (`build/alpha15-legacy14-reload.log`, 57 s),
|
||||
avec **38 pièces**, **5 coffres non ouverts**, le bloc témoin et le journal
|
||||
conservés. Cette réouverture exclut la recompilation : elle vérifie les mêmes
|
||||
binaires que la création, pendant le développement séparé du nouveau planner15.
|
||||
Les classes historiques de planification et de rendu ne sont pas modifiées.
|
||||
Les rapports sont conservés dans `build/alpha15-evidence/legacy14-*.json`.
|
||||
|
||||
Le premier test moteur15 a vérifié les accès des bâtiments et réseaux, puis a
|
||||
échoué sur une hypothèse insuffisante de son ordonnanceur de fluides. Le test
|
||||
attend maintenant que les chunks et leurs entités soient prêts, observe un
|
||||
écoulement natif dans des témoins temporaires fermés, puis contrôle les bassins
|
||||
naturels après des ticks mesurés. Ces témoins sont séparés des équipements
|
||||
naturellement générés et supprimés avant sauvegarde.
|
||||
Les essais sur modèles contrôlés seront distingués des équipements effectivement
|
||||
générés sur terrain naturel. La validation Windows native reste à effectuer.
|
||||
|
||||
### Contrôles ciblés du retour d’essai anticipé
|
||||
|
||||
Les smokes exécutés le 9 septembre passent avec les classes Minecraft exactes :
|
||||
les six familles d’intérieurs dans quatre orientations contrôlent la collision
|
||||
des portes ouvertes, le côté des dossiers des sièges et l’appui plein des
|
||||
lanternes (`build/alpha15-targeted-furnishings.log`). Le rendu de surface15
|
||||
corrige aussi les bancs et ajoute les marches sur le sol existant, leur sommet
|
||||
rejoignant le niveau voisin. Dégagement des rues, volumes de protection,
|
||||
grande salle, boucle alternative et fondations souterraines passent dans
|
||||
`build/alpha15-targeted-utilities.log`. Ces essais contrôlés ne remplacent pas
|
||||
le scénario natif final.
|
||||
|
||||
Le scénario de reprise certifiée du vide de génération15 passe sa création et
|
||||
ses deux réouvertures dans des JVM séparées : **154 chunks FULL**, tous éclairés
|
||||
correctement, et bloc témoin conservé. Les refus de bloc, coffre, entité ou tick
|
||||
présents et la conservation des régions face à une mutation étrangère passent.
|
||||
Rapports : `build/alpha15-evidence/void-reuse-*.json`. Ces preuves précèdent la
|
||||
révision compacte du seul placement urbain ; le test complet final reste exigé.
|
||||
|
||||
### Ville compacte : création finale vérifiée
|
||||
|
||||
La création finale passe sur la graine **0**, île **724**, le 9 septembre
|
||||
2026 : `build/alpha15-final-city-create.log`, processus **26229**, **3 min 19 s**
|
||||
pour les quatre smokes puis le scénario moteur. Cette durée inclut les contrôles,
|
||||
les prospects et la préparation d’un continent ; ce n’est pas un benchmark de
|
||||
chargement joueur ou de rendu.
|
||||
|
||||
- Racine : un quartier natif au chunk **(-2, 3)**, cinq lots (immeuble, hôtel,
|
||||
bibliothèque, jardin et place), **33 pièces de rues/carrefours**, **52 pièces
|
||||
natives** avec le sous-sol. Les cinq entrées rejoignent le même réseau ;
|
||||
**393 sections** de rues sont traversées sur les seules rues et seuils.
|
||||
L’immeuble est accessible en **(-30, 252, 21)** sur cette graine.
|
||||
- Sous la racine : un accès, quatre collecteurs, sept passages, une pool room
|
||||
et une grande salle sèche. Le socle et les quatre secteurs de la salle sont
|
||||
accessibles depuis la trappe de surface.
|
||||
- Continent témoin **256**, graine **8737523517608206795**, centre **(2112, 320)** :
|
||||
restaurant et immeuble, réseau de rues, pont de 17 blocs, garage et pool room
|
||||
souterrains. **24 pièces natives** et **5 appuis souterrains** inspectés.
|
||||
Le pont est parcouru de **(2104, 225, 309)** à **(2120, 224, 309)**.
|
||||
- Les biomes urbains et le spawn naturel de zombies sont vérifiés ; aucun
|
||||
spawner ni villageois ajouté. Cinq coffres restent à butin différé.
|
||||
- Les bassins souterrains sont contrôlés après **120 ticks de monde mesurés** ;
|
||||
19 témoins temporaires indépendants prouvent l’exécution native des fluides,
|
||||
puis sont retirés avant sauvegarde. La piscine de surface est vérifiée dans
|
||||
le premier monde compact, avant la dernière affectation des lots : ses
|
||||
**207 sources** sont conservées, et neuf cellules au-dessus des marches se
|
||||
remplissent normalement sans fuite (216 cellules d’eau au total).
|
||||
|
||||
Le témoin final contient un pont court et des appuis souterrains. Les grands
|
||||
viaducs sur piles construites restent validés sur terrains contrôlés ; ils ne
|
||||
sont pas présentés comme observés dans ce monde naturel. Aucune famille de
|
||||
bâtiments, piscine, garage, salle ou pont n’est garantie sur chaque graine.
|
||||
Rapport : `build/alpha15-evidence/final-city-create.json` ; le monde compact
|
||||
précédent est conservé dans `compact-first-create.json`.
|
||||
|
||||
La réouverture finale du même monde passe dans un autre processus (**26452**,
|
||||
`build/alpha15-final-city-reload.log`, **51 s**) : **76 pièces natives**, **5 coffres
|
||||
à butin différé**, **108 points de parcours intérieurs** et **5 appuis** conservés.
|
||||
Les sept entrées restent reliées aux rues, le bloc témoin **(-38, 252, 21)** est
|
||||
présent et le journal garde exactement ses octets. Rapport :
|
||||
`build/alpha15-evidence/final-city-reload.json`.
|
||||
|
||||
`check build assemblePack` final passe en **2 min 6 s** le 9 septembre 2026,
|
||||
avec les **huit tests moteur requis** et tous les smokes, dont les quatre
|
||||
contrôles15 (`build/alpha15-final-check-build-pack.log`). Le JAR et le pack
|
||||
sont construits depuis les mêmes sources que le scénario final. Le changement
|
||||
reste réservé à15 ; la revue finale ne trouve aucune modification des quatre
|
||||
classes de planification/rendu/pièce historiques14.
|
||||
|
||||
La release immuable `v0.1.0-alpha.15`, source `dd9c795`, et le canal `6cf54ee`
|
||||
sont publiés. JAR, MRpack et amorçage sont vérifiés ; deux installations isolées
|
||||
et deux synchronisations de la même instance Prism réussissent. Les **767**
|
||||
fichiers personnels et réglages suivis restent identiques. L’ancien JAR d’essai
|
||||
est conservé séparément ; un seul JAR Sanctuary est actif. Voir le
|
||||
[reçu de livraison](packwiz.md#alpha15-publiée--9-septembre-2026).
|
||||
@@ -1,144 +0,0 @@
|
||||
# Alpha.16 — Sanctuary habitée avant nous
|
||||
|
||||
**État : publiée et installée dans la même instance Prism.** Ticket WG-19,
|
||||
branche `codex/sanctuary-ruins-alpha16`. Minecraft 26.3-pre-2, Fabric Loader 0.19.5,
|
||||
Fabric API 0.160.0+26.3 et Java 25, comme l’alpha.15.
|
||||
|
||||
## Contrat de génération
|
||||
|
||||
La génération 16 demande un **nouveau monde Sanctuary**. Le choix public reste
|
||||
unique, avec le bouton natif Personnaliser : Petit 512, Moyen 724 par défaut,
|
||||
Grand 1024. Les codecs `island_v13`, `island_v14`, `island_v15`, leurs pièces
|
||||
natives, journaux, biomes et calculs de chunks futurs sont conservés. Aucun
|
||||
monde personnel n’est converti, ouvert par les tests ou régénéré.
|
||||
|
||||
Lorsque les structures sont activées, l’île de départ reçoit une ville
|
||||
compacte. Une recherche naturelle est suivie, si nécessaire, d’une réservation
|
||||
architecturale locale 49 × 49 : rues et bâtiments reliés au-dessus d’un socle
|
||||
creux, avec appuis de maçonnerie certifiés. Cette réservation peut dégager le
|
||||
terrain et remplacer l’eau dans sa seule emprise, avant le placement des pièces.
|
||||
Elle ne modifie pas la densité de toute l’île et ne s’applique pas aux continents.
|
||||
Les expansions conservent leur profil et leur placement soumis au terrain.
|
||||
|
||||
La passe souterraine comprend une piscine couverte avec bassin de 50 × 25
|
||||
blocs et quatre blocs de profondeur, des égouts ouverts et des parkings de
|
||||
3, 4, 6 ou 8 plateformes selon les appuis disponibles. Une grande salle sèche
|
||||
peut compléter le réseau. Chaque équipement dépend des volumes et appuis
|
||||
certifiés sur le terrain ; le tableau
|
||||
ci-dessous distingue les témoins natifs des cas de modèle. Les anciens petits
|
||||
bassins de la génération 15 restent inchangés.
|
||||
|
||||
Les plans des étages alternent réellement. Les sanitaires ont des parois
|
||||
complètes, leur porte et leur plafond ; les lits sont adossés aux murs avec
|
||||
une rive latérale accessible. Portes, sièges et lanternes conservent leurs
|
||||
contrôles d’orientation et d’appui.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Les cinq smokes 16 passent : intérieurs, planification, fondation, protection
|
||||
et sous-sols. Ils vérifient notamment les quatre orientations, les plans
|
||||
d’étages, les sanitaires, les lits et les appuis.
|
||||
- Matrice de planification moteur : les trois tailles 512/724/1024 × graines 0/1
|
||||
passent. Une ville de 3 à 8 lots, avec rues et entrées reliées dans les six cas.
|
||||
Ces six reliefs trouvent une implantation naturelle ; le recours architectural
|
||||
est couvert séparément par les terrains adverses, le rendu borné et le NBT
|
||||
des smokes. Rapport : `build/alpha16-evidence/planning-matrix.json`.
|
||||
- Création native 16, graine 0, taille Moyenne, puis réouverture dans une seconde
|
||||
JVM : **71 pièces, 5 coffres différés, 107 cibles de parcours et 74 appuis**
|
||||
conservés entre l’île et une expansion de 256 blocs. Les hashes des starts,
|
||||
le journal et le bloc d’or témoin restent identiques. Processus 29685 puis 29853.
|
||||
Rapports : `build/alpha16-evidence/final-city-create.json` et
|
||||
`final-city-reload.json` ; logs `build/alpha16-third-city-create.log` et
|
||||
`alpha16-city-reload.log`.
|
||||
- Après **120 ticks du monde réellement observés**, la piscine garde ses
|
||||
**4 976 cellules d’eau** ; les deux égouts observés déversent de l’eau au-delà
|
||||
de leur bouche. Dix-neuf témoins temporaires indépendants prouvent l’activité
|
||||
effective du moteur des fluides, puis sont supprimés avant sauvegarde. Les
|
||||
cascades atteignent des corniches : elles ne sont pas toutes libres jusqu’au
|
||||
vide inférieur. Aucun test ne présente une simple attente comme un écoulement.
|
||||
- Compatibilité 15 : création puis réouverture sous le mod 16 réussies,
|
||||
76 pièces natives, 5 coffres, 108 cibles de parcours et 5 appuis conservés.
|
||||
Les SHA-256 des deux starts et du journal sont exactement ceux de l’alpha.15
|
||||
publiée. Les fichiers de génération 15 restent inchangés. Le preset caché
|
||||
`sanctuary_v15.json` est identique à l’ancien preset public, SHA-256
|
||||
`6c4a91eede4b9978526d6de6289dbc7394ed4114ebcae9f779fac3ce7e7d887e`.
|
||||
Rapports : `build/alpha16-evidence/legacy15-create.json` et `legacy15-reload.json`.
|
||||
- `./gradlew check build assemblePack` passe en 2 min 18 s : huit GameTests,
|
||||
tous les smokes et index du pack vérifiés. Log :
|
||||
`build/alpha16-final-check-build-pack.log`.
|
||||
- JAR, MRpack et ZIP d’amorçage vérifiés : même version et même JAR, index et
|
||||
hashes cohérents, aucun code de test embarqué. SHA-256 du JAR :
|
||||
`5504018c46300accf01478dbc8bfb8e216395a5435cf7d7f501005cbc65b8e43`.
|
||||
|
||||
### Distribution des sous-sols sur six reliefs
|
||||
|
||||
Un second relevé utilise le vrai bruit Minecraft et les protections hydrologiques,
|
||||
sans générer les chunks de la ville. Il contrôle les volumes, les fondations et
|
||||
les limites de calcul ; les fluides et la persistance restent prouvés par le
|
||||
scénario natif ci-dessus. Rapport : `build/alpha16-evidence/underground-matrix.json`.
|
||||
|
||||
| Diamètre | Graine | Piscine | Grande salle | Parking | Égout ouvert |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 512 | 0 | Absente | Présente | 8 niveaux | Présent |
|
||||
| 512 | 1 | Absente | Présente | 8 niveaux | Présent |
|
||||
| 724 | 0 | Présente | Présente | 8 niveaux | Présent |
|
||||
| 724 | 1 | Absente | Présente | 8 niveaux | Présent |
|
||||
| 1024 | 0 | Présente | Absente | 8 niveaux | Présent |
|
||||
| 1024 | 1 | Présente | Présente | 8 niveaux | Présent |
|
||||
|
||||
La piscine est donc présente dans trois cas sur six, la salle dans cinq cas.
|
||||
Les six réseaux ont un accès depuis la surface. Ce petit échantillon ne donne
|
||||
pas une fréquence statistique ni une garantie pour les autres graines. Le
|
||||
budget souterrain atteint son plafond de 420 000 sondes dans quatre cas ;
|
||||
la recherche s’arrête alors en conservant les équipements déjà certifiés.
|
||||
Une aile absente ne prouve donc pas l’absence de tout emplacement compatible :
|
||||
une autre solution peut se trouver au-delà du budget de recherche.
|
||||
La passe souterraine prend 2,9 à 7,0 secondes après la planification de surface
|
||||
sur cette machine ; cela ne mesure ni le rendu client ni le chargement complet.
|
||||
|
||||
## Repères pour l’essai en jeu
|
||||
|
||||
Créer un nouveau monde **Sanctuary, Moyen, graine 0**, structures activées.
|
||||
Les coordonnées suivantes proviennent des blocs réellement générés et relus.
|
||||
Elles ne constituent pas une migration d’un monde précédent.
|
||||
|
||||
| Lieu | Repère X / Y / Z |
|
||||
| --- | --- |
|
||||
| Entrée de l’immeuble | −30 / 252 / 21 |
|
||||
| Trappe depuis la rue | **−19 / 252 / 41** |
|
||||
| Promenade de la piscine | −45 / 230 / −11 |
|
||||
| Grande salle sèche | −42 / 231 / 20 |
|
||||
| Parking, niveau supérieur | 10 / 230 / 20 |
|
||||
| Parking, niveau inférieur | 10 / 118 / 20 |
|
||||
| Bouche de l’égout inférieur | **−50 / 117 / 20** |
|
||||
|
||||
Le bassin remplit X −44 à 5, Y 225 à 228, Z −23 à 1. Les plateformes du
|
||||
parking sont à Y 117, 133, 149, 165, 181, 197, 213 et 229. Le réseau est
|
||||
accessible depuis la trappe de rue. Sur ce témoin, la cascade atteint une
|
||||
corniche à Y 113. L’expansion de référence `city16_0_2`, graine
|
||||
9111000990819250332, possède aussi une bouche à 1958 / 202 / −651.
|
||||
|
||||
Les équipements restent conditionnés par leurs volumes et appuis ; seule la
|
||||
zone de ville initiale est garantie par le recours architectural. Aucun essai Windows natif ni revue visuelle en client n’a été effectué pour
|
||||
cet incrément ; les vérifications ci-dessus proviennent du moteur serveur Mac. Le pack doit encore être validé en jeu par Koka.
|
||||
|
||||
## Livraison
|
||||
|
||||
Le tag `v0.1.0-alpha.16` vise le commit source `98fc13c` ; le canal packwiz
|
||||
est au commit `927c862`. Les trois artefacts publics sont vérifiés par
|
||||
téléchargement, puis deux installations isolées et deux synchronisations de
|
||||
la même instance Prism passent. Un seul JAR alpha.16 est actif ; les 767
|
||||
fichiers personnels et réglages suivis conservent leurs hashes. Aucun monde
|
||||
personnel ouvert ou régénéré. Les reçus et la sauvegarde ciblée sont décrits
|
||||
dans [Distribution](packwiz.md).
|
||||
|
||||
## Suite validée avec Koka, hors alpha.16
|
||||
|
||||
Développer les trois familles : mines et ateliers, lieux étranges,
|
||||
infrastructures à réactiver. Plusieurs réseaux distincts se connectent parfois.
|
||||
Les installations encore utilisables et les chantiers interrompus racontent
|
||||
l’occupation passée par d’autres joueurs. Les back rooms de ces donjons sont
|
||||
physiquement dans l’île et restent distinctes de la dimension des objets perdus.
|
||||
Le butin combinera des reliques collectives uniques et des récompenses
|
||||
personnelles. Les règles d’attribution restent à définir ; aucun réacteur,
|
||||
nouveau spawner de donjon ou système de progression n’est ajouté ici.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Alpha.17 — réseaux oubliés, prototype réversible
|
||||
|
||||
**État : publié et installé dans la même instance Prism.** Ticket WG-20, branche
|
||||
`codex/structures-experiment-alpha17`. Minecraft 26.3-pre-2, mêmes dépendances
|
||||
que l’alpha.16. Cet essai porte sur les lieux et leur génération.
|
||||
|
||||
## Contrat
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, puis **Personnaliser** : Petit 512,
|
||||
Moyen 724 par défaut, Grand 1 024 et **Structures expérimentales** (activées par
|
||||
défaut). Le choix est enregistré dans le générateur 17, avec la taille. Désactiver
|
||||
l’option pour un autre nouveau monde permet de comparer avec le socle urbain 16.
|
||||
Ce bouton ne supprime ni ne rajoute de structures dans une sauvegarde existante.
|
||||
|
||||
Les anciens mondes gardent leurs codecs, rendus, pièces et futurs chunks. Le
|
||||
preset 16 historique est conservé à l’identique et caché à la création. Le 17
|
||||
possède son propre journal ; aucune conversion ni rétrogradation de sauvegarde
|
||||
n’est proposée. Le retour au binaire 16 concerne seulement les mondes 16 et
|
||||
antérieurs, jamais un monde 17.
|
||||
|
||||
Les nouvelles structures sont recherchées sur l’île initiale uniquement, hors
|
||||
ville et eaux protégées. Trois petits réseaux distincts : mines et ateliers,
|
||||
salles étranges, installations techniques abandonnées. Salles et couloirs se
|
||||
raccordent à des accès de surface. Les fondations reposent sur le terrain ou sur
|
||||
des appuis courts certifiés. La présence dépend des emplacements trouvés dans
|
||||
un budget borné. Les jonctions entre familles restent une étape suivante.
|
||||
|
||||
Les établis, fourneaux et outils vanilla donnent des usages aux installations ;
|
||||
rails, soutènements et chantiers interrompus suggèrent les anciens occupants.
|
||||
Les lieux étranges sont physiques dans l’île, distincts de la future dimension
|
||||
Backrooms. Aucun réacteur, progression, relique collective, récompense personnelle
|
||||
ou nouveau spawner n’est ajouté par ce prototype. Les coffres éventuels utilisent
|
||||
les tables de butin vanilla.
|
||||
|
||||
## Vérification et essai
|
||||
|
||||
Le monde **Moyen, graine 0**, option expérimentale activée, reçoit trois réseaux
|
||||
avec cinq pièces chacun. Les contrôles natifs portent sur **15 pièces**, **7 639
|
||||
blocs**, **13 chunks FULL**, **12 appuis**, **49 cibles de parcours** et **3 coffres
|
||||
à butin différé**. Les empreintes des starts, le plan et un bloc d’or témoin sont
|
||||
conservés après réouverture dans une seconde JVM. Les trappes sont ouvertes pour
|
||||
vérifier le parcours réel d’un joueur, puis refermées avant la sauvegarde.
|
||||
|
||||
| Réseau | Entrée X / Y / Z |
|
||||
| --- | --- |
|
||||
| Mine et atelier | **78 / 252 / 179** |
|
||||
| Salles étranges et archives | **−194 / 254 / −22** |
|
||||
| Locaux techniques | **116 / 253 / −157** |
|
||||
|
||||
Avec les commandes autorisées :
|
||||
|
||||
```text
|
||||
/locate structure sanctuary:forgotten_networks
|
||||
```
|
||||
|
||||
La commande donne une entrée de surface. Le prototype recherche les trois
|
||||
familles, sans les garantir sur chaque graine. Les neuf types de salles et
|
||||
couloirs plus l’accès sont testés sur des dimensions et ouvertures variées ;
|
||||
les plafonds, appuis de mobilier, orientations et chemins sont contrôlés.
|
||||
|
||||
Les trois smokes dédiés passent. Les huit contrôles moteur du socle passent,
|
||||
avec conservation des cavités et eaux naturelles. Les trois tailles et les deux
|
||||
valeurs de l’option passent le stockage réel des paramètres sur disque.
|
||||
|
||||
La planification des réseaux prend environ une seconde après celle de la ville
|
||||
sur le témoin 0 de ce Mac : 915 colonnes lues et 11 974 sondages, pour des plafonds
|
||||
respectifs de 5 000 et 250 000. Cela ne mesure ni le rendu client ni le chargement
|
||||
complet sur une autre machine. Rapports locaux ignorés :
|
||||
`build/alpha17-evidence/ruins17-create.json` et `ruins17-reload.json`.
|
||||
Logs : `build/alpha17-native-create-final.log`, `alpha17-native-reload.log`,
|
||||
`alpha17-first-native-base.log`, `alpha17-planner-piece-smokes.log`.
|
||||
|
||||
L’essai natif avec **Structures expérimentales désactivées** passe : plan vide,
|
||||
zéro sondage expérimental et booléen conservé. Rapport :
|
||||
`build/alpha17-evidence/ruins17-disabled.json`, log `build/alpha17-native-disabled.log`.
|
||||
La création et la réouverture actives utilisent les processus 32029 et 32123 ; le
|
||||
cas désactivé utilise 32176. Aucune sauvegarde personnelle n’a été ouverte.
|
||||
|
||||
La création et la réouverture des mondes de génération **16** passent sous le
|
||||
binaire 17 : les 71 pièces de la ville initiale et du continent témoin gardent
|
||||
leurs empreintes natives, ainsi que le journal, les paramètres et le bloc d’or
|
||||
témoin de la version publiée. Rapports :
|
||||
`build/alpha17-evidence/legacy16-create.json` et `legacy16-reload.json`.
|
||||
Le preset 16 historique est également identique octet par octet. La comparaison
|
||||
avec les reçus publiés est consignée dans `legacy16-comparison.json` :
|
||||
création sous 17 dans le processus 32244, réouverture dans 32429.
|
||||
|
||||
`./gradlew check build assemblePack` passe : 52 tâches, huit GameTests moteur
|
||||
et les trois smokes du prototype. Le JAR de production exclut les classes de
|
||||
test ; les versions, les hashes du pack, le MRpack et le ZIP d’amorçage Prism
|
||||
sont vérifiés. Journal final : `build/alpha17-final-check-build-pack.log`.
|
||||
|
||||
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
|
||||
deux dans la même instance Prism passent ; les 767 fichiers personnels et
|
||||
réglages suivis gardent leurs hashes. Le JAR 16 et le suivi packwiz précédent
|
||||
sont sauvegardés hors de `mods/`. Voir [le reçu de livraison](packwiz.md).
|
||||
|
||||
Aucun essai Windows natif ni revue visuelle dans le client n’est revendiqué ;
|
||||
le prochain essai de Koka validera l’ambiance.
|
||||
@@ -1,173 +0,0 @@
|
||||
# Alpha.19 — anciennes infrastructures de Sanctuary
|
||||
|
||||
**État : alpha.19 publiée, même instance Prism synchronisée et vérifiée.** Ticket WG-22, branche
|
||||
`codex/heritage-worldgen-alpha19`. Minecraft 26.3-pre-2, mêmes dépendances que
|
||||
l’alpha.17. La numérotation 19 est demandée explicitement ; aucune 18 n’est
|
||||
intercalée dans ce dépôt.
|
||||
|
||||
## Périmètre et réversibilité
|
||||
|
||||
Première passe des quatorze lieux du [catalogue](structures-audit.md) : gare de
|
||||
marchandises, centre postal, dépôt communal, cartothèque, observatoire,
|
||||
maintenance ferroviaire, pompage, sous-station, serre, quai d’expédition,
|
||||
relais radio, club photo, vestiaires/salles étranges et refuge de chantier.
|
||||
Les plans recherchent des sites admissibles et des voies de fret ou chemins
|
||||
entre destinations réelles ; ils ne garantissent pas tous les lieux sur chaque
|
||||
graine. Les mesures et coordonnées des essais figurent ci-dessous.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** et utiliser **Personnaliser** :
|
||||
Petit 512, Moyen 724 par défaut ou Grand 1 024, avec **Structures expérimentales**.
|
||||
L’option est activée par défaut et enregistrée avec le générateur 19. La désactiver
|
||||
pour un autre nouveau monde conserve le socle urbain sans les réseaux
|
||||
expérimentaux 17 et les infrastructures 19. Elle ne retire aucun bloc ni ne change
|
||||
les règles d’une sauvegarde existante.
|
||||
|
||||
Les mondes 17 et antérieurs gardent leurs codecs, plans, pièces et futurs chunks.
|
||||
Le preset 17 public est conservé octet par octet sous un identifiant historique
|
||||
caché à la création. Le générateur 19 et son journal sont distincts. Il n’existe
|
||||
aucune conversion ou rétrogradation de sauvegarde 19 vers un ancien binaire.
|
||||
L’ancienne release reste disponible pour ses propres mondes, et l’option d’un
|
||||
nouveau monde permet de comparer les deux configurations du nouveau binaire.
|
||||
|
||||
Les nouveaux lieux sont réservés à l’île initiale. La ville et les anciens
|
||||
réseaux servent de base ; leurs protections et celles de l’hydrologie restent
|
||||
respectées. Les liaisons vers de futurs continents seront construites après
|
||||
leur apparition, sans présumer leurs emplacements dans cette génération.
|
||||
|
||||
## Objets et systèmes
|
||||
|
||||
Les bâtiments suggèrent leurs usages futurs par leurs pièces et aménagements.
|
||||
Mailbox, deposit boxes, caméra, réacteur, progression et récompenses
|
||||
personnelles ne sont pas activés. Chaque nouveau lieu dispose d’une table
|
||||
de coffre propre à la génération 19, avec un outil vanilla lié à son usage et
|
||||
quelques fournitures. Les coffres sont partagés et ne se renouvellent pas.
|
||||
|
||||
La valeur des rails, équipements et blocs récupérables du décor s’ajoute au
|
||||
butin. Leur distribution doit être constatée sur les témoins, sans transformer
|
||||
cette première passe en une garantie d’équilibrage économique pour la bêta.
|
||||
|
||||
## Vérifications effectuées
|
||||
|
||||
- Placement des quatorze architectures, ouvertures, circulation, orientation
|
||||
et appuis du mobilier ; accès réel au coffre et retour possible.
|
||||
- Plans bornés sur les trois tailles, terrain non occupé, supports, continuité
|
||||
des tracés et distinction entre ballast praticable et rails retirés.
|
||||
- Pièces natives et références locales, y compris pour les longues voies ;
|
||||
géométrie, orientation et rails sauvegardés dans un schéma strict.
|
||||
- Création et réouverture d’un monde 19 avec modification témoin ; conservation
|
||||
des starts, butins différés, accès, routes et supports.
|
||||
- Option désactivée, paramètres sauvegardés, génération 17 comparée aux preuves
|
||||
de la version publiée, refus des migrations implicites de journaux.
|
||||
- `./gradlew check build assemblePack`, artefacts et index, publication immuable
|
||||
puis canal packwiz et deux synchronisations de la même instance Prism.
|
||||
|
||||
Les smokes des quatorze architectures, de leurs quatre orientations, des
|
||||
plans sur les trois tailles et des pièces natives passent. Le mode désactivé
|
||||
est vérifié dans un monde 19 neuf : aucun site ni liaison expérimental.
|
||||
`./gradlew check build assemblePack` passe, avec les huit GameTests généraux,
|
||||
125 chunks FULL, 5 923 blocs d’eau conservés et 1 800 ticks de fluides.
|
||||
Le JAR, le MRpack, les hashes du pack et le ZIP Prism sont vérifiés.
|
||||
La release et le canal packwiz sont publiés. Deux synchronisations isolées puis
|
||||
deux dans la même instance Prism passent ; 767 fichiers personnels et réglages
|
||||
suivis conservent leurs hashes. Le JAR précédent et le suivi packwiz sont
|
||||
sauvegardés dans `sanctuary-backups/before-0.1.0-alpha.19/`.
|
||||
Voir [le reçu de distribution](packwiz.md#alpha19-publiée--9-septembre-2026).
|
||||
Aucun essai visuel dans le client ni essai Windows natif n’a été effectué.
|
||||
|
||||
## Témoin natif Moyen, graine 0
|
||||
|
||||
Créer un nouveau monde avec **Structures expérimentales activées**.
|
||||
`/sanctuary structures` liste les entrées prévues sans charger de chunks ;
|
||||
`/locate structure sanctuary:heritage_networks` retrouve l’entrée d’un site.
|
||||
Le relevé ci-dessous provient des placements et parcours vérifiés par le moteur.
|
||||
|
||||

|
||||
|
||||
Treize des quatorze lieux apparaissent. **La cartothèque est absente de cette
|
||||
graine** : la recherche n’a pas trouvé d’emplacement admissible. La ville et les
|
||||
trois réseaux souterrains 17 sont conservés. Les raccords recherchés vers la
|
||||
ville et l’ancienne mine ne sont pas admis sur ce témoin.
|
||||
|
||||
Les sept liaisons comprennent un tracé de fret **dépôt → pompage de 499 blocs**,
|
||||
d’une étendue de 245 blocs sur X et 181 sur Z. Son ballast est praticable ;
|
||||
478 rails sont présents et 21 sont volontairement manquants. Les autres voies
|
||||
et chemins relient gare, dépôt, atelier, poste, club photo, serre et vestiaires.
|
||||
Le quai, l’observatoire, le relais, la sous-station et le refuge gardent leurs
|
||||
propres accès au terrain, même sans liaison avec ce réseau.
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent chacune les deux
|
||||
GameTests : **112 pièces natives, 111 chunks FULL, 80 487 blocs contrôlés,
|
||||
2 882 contrôles de portance, 13 coffres différés et 68 cibles accessibles**.
|
||||
Trois tirages de chacune des quatorze tables de butin sont vérifiés
|
||||
séparément, sans ouvrir les coffres persistants. Une modification témoin à
|
||||
`-204 / 255 / -13` est conservée à la réouverture.
|
||||
|
||||
Empreinte stable du plan :
|
||||
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`.
|
||||
JVM de création `36366`, de réouverture `36481`. Reçus locaux ignorés :
|
||||
`build/alpha19-evidence/heritage19-create.json` et `heritage19-reload.json`.
|
||||
|
||||
La planification supplémentaire utilise 15 144 colonnes natives, dans un plafond
|
||||
de 16 000, en 20,7 s sur le Mac de développement. Ce temps concerne ce plan,
|
||||
pas le chargement total du jeu. Les routes sont découpées par chunk ; aucun
|
||||
start ne couvre toute leur longueur ni ne charge une destination lointaine.
|
||||
|
||||
La prospection 19 consulte les plans hydrologiques de toute l’enveloppe de
|
||||
départ, avant de publier les sites. Le contrôle général utilise donc pour 19
|
||||
une limite géographique calculée : 5 régions possibles pour Petit, 9 pour
|
||||
Moyen/Grand, avec les budgets de colonnes et de sondes contrôlés séparément.
|
||||
Le témoin Moyen en consulte 5 ; les quatre régions périphériques représentent
|
||||
3,38 s de calcul hydrologique supplémentaire dans le relevé du build.
|
||||
Ces consultations ne matérialisent aucun chunk. Les générations historiques
|
||||
conservent le contrôle initial de 4 régions maximum autour du spawn.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryHeritage19Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
## Autres tailles et couverture du catalogue
|
||||
|
||||
Les deux GameTests natifs passent aussi sur Petit et Grand, graine 42.
|
||||
Les quatorze architectures ont ainsi chacune été rencontrées et parcourues
|
||||
sur le terrain réel, en complément des rotations vérifiées par les smokes.
|
||||
|
||||
| Taille et graine | Lieux | Liaisons | Pièces natives | Chunks FULL |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Petit 512, 42 | 10 | 2 | 35 | 44 |
|
||||
| Moyen 724, 0 | 13 | 7 | 112 | 111 |
|
||||
| Grand 1 024, 42 | 13 | 4 | 47 | 61 |
|
||||
|
||||
Pour visiter la cartothèque : **Petit, graine 42**, entrée
|
||||
`-101 / 251 / -25`, ou **Grand, graine 42**, entrée `-252 / 249 / 146`.
|
||||
Le Petit testé n’a ni poste, ni observatoire, ni atelier ferroviaire, ni quai.
|
||||
Le Grand testé n’a pas de quai. Aucun grand axe n’est admis sur ces deux témoins :
|
||||
les portées maximales de leurs liaisons sont respectivement de 32 et 55 blocs.
|
||||
Le témoin Moyen, graine 0, reste celui conseillé pour découvrir le grand fret.
|
||||
Ces résultats rendent explicite la variation de cette première passe ; ils ne
|
||||
constituent pas une garantie de présence ou de connectivité pour chaque graine.
|
||||
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/small42-create.json` (JVM 36591)
|
||||
et `large42-create.json` (JVM 36669).
|
||||
|
||||
## Compatibilité 17 vérifiée
|
||||
|
||||
Deux JVM de développement distinctes créent puis rouvrent le témoin 17
|
||||
**Moyen, graine 0**, avec le nouveau binaire. Les deux GameTests passent dans
|
||||
chaque exécution. Le codec, les starts natifs, les butins différés et les
|
||||
parcours restent identiques aux preuves de l’alpha.17 publiée. Le bloc d’or
|
||||
placé par le test est conservé à la réouverture.
|
||||
|
||||
Empreinte du plan 17 :
|
||||
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
|
||||
Les trois starts restent ceux documentés dans [le témoin 17](testing-alpha17.md).
|
||||
Reçus locaux ignorés : `build/alpha19-evidence/legacy17-create.json` et
|
||||
`legacy17-reload.json`.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryRuins17Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
Ces commandes visent uniquement le monde de développement ignoré. La première
|
||||
le recrée ; la seconde conserve ce même monde et lit ses preuves précédentes.
|
||||
@@ -1,327 +0,0 @@
|
||||
# Alpha.20 — identité architecturale
|
||||
|
||||
**État : alpha.20 publiée, même instance Prism synchronisée et vérifiée.**
|
||||
Ticket WG-23, branche `codex/architecture-alpha20`.
|
||||
Minecraft 26.3-pre-2, dépendances identiques à la version 19 publiée.
|
||||
|
||||
## Intention
|
||||
|
||||
Chaque fonction reçoit une palette et une composition propre : bois clairs et
|
||||
grandes baies des lieux publics, pierre et tuf des installations techniques,
|
||||
loggias et retraits des logements. Les détails utilisent les blocs vanilla :
|
||||
tuf ciselé, briques de tuf ciselées, briques de pierre sculptées, bois et
|
||||
planches de chêne/bouleau, granit, diorite et andésite. Moulures, liserés,
|
||||
encadrements, murs bas et corniches utilisent escaliers, dalles et trappes.
|
||||
Les fondations restent en briques de pierre. Le [catalogue architectural](architecture-alpha20.md)
|
||||
décrit les formes et les matériaux effectivement construits pour chaque usage.
|
||||
|
||||
La passe concerne les quatorze infrastructures, la ville et ses sous-sols,
|
||||
ainsi que les petits réseaux oubliés. Elle ne déplace pas les sites ni les
|
||||
routes pour obtenir ses silhouettes. Les plans 19/17/16 servent d’emprises
|
||||
certifiées aux nouveaux rendus 20.
|
||||
|
||||
## Connexions des blocs
|
||||
|
||||
La géométrie complète d’une pièce est assemblée avant le découpage par chunk.
|
||||
Les états des vitres, grilles, clôtures et murets sont calculés à partir des voisins
|
||||
réellement présents dans ce plan ; les escaliers reçoivent leurs angles.
|
||||
Cela évite d’écrire des vitres sans raccord malgré leur juxtaposition, ou de
|
||||
dépendre de l’ordre de chargement des chunks voisins. Aucun chunk extérieur
|
||||
n’est chargé pour compléter ces états.
|
||||
|
||||
## Sauvegardes et réversibilité
|
||||
|
||||
Créer un **nouveau monde Sanctuary** pour voir la génération 20. Le bouton
|
||||
natif **Personnaliser** garde Petit 512, Moyen 724 et Grand 1 024.
|
||||
**Structures expérimentales** reste activé par défaut, enregistré avec le
|
||||
monde ; le désactiver dans un autre nouveau monde garde la ville sans les
|
||||
réseaux expérimentaux. Les détails urbains de la version 20 restent présents.
|
||||
|
||||
Le nouveau codec `sanctuary:island_v20`, son journal et ses types de pièces
|
||||
distinguent les nouveaux rendus des précédents. Les mondes 19 et antérieurs
|
||||
gardent leurs codecs, leurs pièces et leurs futurs chunks. Aucun monde
|
||||
personnel n’est ouvert ni rénové par la mise à jour. Une sauvegarde 20 ne se
|
||||
rétrograde pas vers un ancien binaire ; la release 19 reste disponible pour
|
||||
ses propres mondes.
|
||||
|
||||
## Vérification avant livraison
|
||||
|
||||
- Parcours des entrées, pièces et coffres, quatre orientations, marches,
|
||||
supports et limites d’écriture pour chaque type.
|
||||
- Connexions des vitres et grilles, angles d’escaliers, comparaison du rendu
|
||||
complet et du rendu découpé selon plusieurs ordres de chunks.
|
||||
- Création et réouverture natives, option désactivée, ancien témoin 19.
|
||||
- `check build assemblePack`, intégrité des artefacts, publication immuable,
|
||||
deux synchronisations isolées puis deux dans la même instance Prism.
|
||||
|
||||
## Reçus natifs : infrastructures et réseaux oubliés
|
||||
|
||||
**Création et réouverture réussies**, dans deux JVM distinctes, pour un monde
|
||||
jetable **Moyen 724, graine 0**, structures et structures expérimentales activées,
|
||||
codec `sanctuary:island_v20`. Les rapports locaux conservés sont
|
||||
`build/alpha20-evidence/heritage20-create.json` (processus 39381) et
|
||||
`build/alpha20-evidence/heritage20-reload.json` (processus 39736).
|
||||
Les journaux correspondants sont `build/alpha20-heritage-create-2.log` et
|
||||
`build/alpha20-heritage-reload.log`. Ces fichiers de développement restent
|
||||
ignorés par Git ; les valeurs utiles sont transcrites ci-dessous.
|
||||
|
||||
| Vérification | Création | Réouverture |
|
||||
| --- | --- | --- |
|
||||
| Sites expérimentaux naturels | 13 types présents sur 14 | Mêmes 13 sites |
|
||||
| Structures natives des sites et voies | 85 starts, 112 pièces | Mêmes empreintes NBT et références |
|
||||
| Positions de blocs contrôlées pour sites et voies | 80 487 | 80 487, hors modification témoin autorisée |
|
||||
| Chunks `FULL` contrôlés pour sites, voies et réseaux oubliés | 120 | 120 |
|
||||
| Contrôles de roche porteuse | 2 882 | 2 882 |
|
||||
| Coffres des sites | 13 butins différés, non ouverts | Tables et graines conservées |
|
||||
| Réseaux oubliés | 3 réseaux, 15 pièces, 7 639 positions contrôlées | Mêmes plans et structures natives |
|
||||
| Coffres des réseaux oubliés | 3 butins différés, non ouverts | Tables et graines conservées |
|
||||
| Parcours depuis les entrées | 68 cibles dans les sites et 49 dans les réseaux oubliés | Parcours revérifiés |
|
||||
| Modification témoin | Bloc d’or placé en `-203 255 -13` | Bloc d’or conservé |
|
||||
|
||||
La cartothèque d’expédition (`EXPEDITION_OFFICE`) n’a pas trouvé d’emplacement
|
||||
sur cette graine. Elle fait partie du catalogue et de ses smokes, mais ce reçu
|
||||
ne constitue pas une preuve de sa génération naturelle. Les quatorze tables
|
||||
de butin sont échantillonnées séparément sans consommer les coffres sauvegardés.
|
||||
Les portes et trappes ouvertes pendant le parcours retrouvent leur état initial.
|
||||
Aucune expansion ni sauvegarde personnelle n’est ouverte dans ces essais.
|
||||
|
||||
Le plan des infrastructures conserve l’empreinte
|
||||
`7f476c2ecca703a05bf88a550c61cc5cbeff2594024c43ccd9f3620bbc2ad3de`
|
||||
entre création et réouverture. Celui des réseaux oubliés conserve
|
||||
`7358ecd7841c03be1977a7f45f37c0c3ef83a32dc27e8d3ec4f0d85d6ee5d3ef`.
|
||||
Les pièces passent aussi un aller-retour NBT exact et refusent un numéro de
|
||||
schéma inconnu.
|
||||
|
||||
### Portée des vérifications de connexion
|
||||
|
||||
Les états complets de **594 blocs détaillés** des infrastructures sont comparés
|
||||
au rendu : 362 vitres ou barreaux, 49 clôtures, 18 murets et 165 escaliers.
|
||||
Sur ce total, **565** sont aussi confrontés aux règles natives de mise à jour
|
||||
des voisins (`updateShape`), avec les tags du serveur chargés. **Onze raccords
|
||||
de vitres ou barreaux traversent une frontière de chunk** et restent connectés.
|
||||
|
||||
Les **29 autres blocs détaillés** ont un voisinage extérieur au plan : leur
|
||||
état sauvegardé est bien comparé au rendu, mais ils sont exclus de cet oracle
|
||||
natif. Le compteur `externalNeighbors=29` désigne ces 29 blocs contrôlés,
|
||||
non un nombre de voisins uniques ; aucune stabilité complète vis-à-vis de ce
|
||||
voisinage extérieur n’est revendiquée. Les réseaux oubliés passent pour leur
|
||||
part **211 comparaisons d’états et 211 vérifications natives** : 6 vitres ou
|
||||
barreaux, 1 clôture et 204 escaliers. Ces résultats sont identiques à la
|
||||
réouverture.
|
||||
|
||||
### Coordonnées du monde témoin
|
||||
|
||||
Créer un nouveau monde **Moyen, graine 0**, avec les deux options de structures
|
||||
activées. Les coordonnées indiquent les pieds à l’entrée, pas le centre d’un
|
||||
toit. `/sanctuary structures` liste les lieux ;
|
||||
`/locate structure sanctuary:heritage_networks`, recherché depuis `0 250 0`
|
||||
dans le test, désigne l’entrée du refuge de chantier.
|
||||
|
||||
| Lieu | Entrée X Y Z |
|
||||
| --- | --- |
|
||||
| Gare de fret | `-110 252 -144` |
|
||||
| Dépôt communal | `-75 254 -184` |
|
||||
| Atelier ferroviaire | `-83 253 -83` |
|
||||
| Château d’eau | `164 252 -3` |
|
||||
| Serre | `173 252 -42` |
|
||||
| Observatoire | `51 251 210` |
|
||||
| Quai sur le vide | `184 181 218` |
|
||||
| Refuge de chantier | `-10 251 -15` |
|
||||
| Réseau minier oublié | `78 252 179` |
|
||||
| Salles étranges oubliées | `-194 254 -22` |
|
||||
| Réseau technique oublié | `116 253 -157` |
|
||||
|
||||
Sept liaisons sont parcourues dans le monde natif. La voie dépôt–château d’eau
|
||||
compte **499 positions**, 478 rails présents et 21 rails volontairement absents,
|
||||
avec une emprise de 245 blocs en X et 181 en Z. Le test prouve un parcours à pied,
|
||||
le ballast et les formes de rails sauvegardées ; il ne prouve pas un trajet
|
||||
ininterrompu en wagonnet sur ces voies abandonnées.
|
||||
|
||||
## Reçu natif : ville, expansion et grands sous-sols
|
||||
|
||||
**Création réussie ; réouverture encore en cours.** Le rapport
|
||||
`build/alpha20-evidence/city20-create.json` (processus 40221), associé au journal
|
||||
`build/alpha20-city-create.log`, couvre deux villes de génération 20 dans un
|
||||
monde de développement jetable. L’île initiale est **Moyen 724, graine 0**.
|
||||
Après trois prospections en lecture seule, une expansion de diamètre **256**
|
||||
est effectivement préparée : `city16_0_2`, graine `9111000990819250332`, centre
|
||||
`2000 -608`, climat `lost_city`. Les deux premiers prospects sont restés sans
|
||||
ville et n’ont pas été activés. L’identifiant de test historique ne change pas
|
||||
la version 20 enregistrée dans cette expansion.
|
||||
|
||||
| Mesure | Ville initiale | Ville d’expansion |
|
||||
| --- | --- | --- |
|
||||
| Pièces natives | 48 | 23 |
|
||||
| Chunks `FULL` inspectés autour des districts | 43 | 19 |
|
||||
| Bâtiments parcourus | Immeuble, hôtel, bibliothèque | Immeuble, hall |
|
||||
| Cibles intérieures et de toit atteintes | 65 | 46 |
|
||||
| Entrées reliées par les rues et les porches seuls | 5 | 2 |
|
||||
| Pièces de rue ou pont traversées | 33 | 17, dont un pont |
|
||||
| Appuis contrôlés | 71 | 3 |
|
||||
| Coffres gardant leur butin différé, non ouvert | 3 | 2 |
|
||||
| Témoins de biome urbain | 38 | 19 |
|
||||
|
||||
Les **71 pièces** sont issues des plans naturels des deux îles. Le parcours
|
||||
urbain ne prend aucun raccourci par le terrain naturel, les intérieurs ou les
|
||||
toits : il confirme la continuité des entrées par les rues dessinées.
|
||||
La table native d’apparition des monstres est vérifiée comme limitée aux
|
||||
zombies vanilla ; cela ne mesure pas un nombre de zombies effectivement apparus.
|
||||
Le pont de l’expansion se traverse de `2000 226 -613` à `2000 224 -597`.
|
||||
Aucune pile de viaduc n’est présente dans ces deux districts.
|
||||
|
||||
Vingt-quatre pièces de test supplémentaires couvrent la sauvegarde des six
|
||||
modèles de bâtiments dans quatre orientations. Ce sont des témoins de
|
||||
sérialisation, pas vingt-quatre bâtiments naturels. Restaurant et caserne,
|
||||
ainsi que la piscine extérieure, ne sont pas présents dans ces deux villes ;
|
||||
leurs smokes ne constituent pas une preuve d’apparition naturelle ici.
|
||||
|
||||
### Grands équipements du témoin initial
|
||||
|
||||
| Repère | Géométrie ou coordonnées constatées |
|
||||
| --- | --- |
|
||||
| Entrée de l’immeuble | `-30 252 21` |
|
||||
| Entrée de l’hôtel | `-8 252 21` |
|
||||
| Entrée de la bibliothèque | `-52 252 43` |
|
||||
| Trappe d’accès aux sous-sols, position du bloc | `-19 252 41` |
|
||||
| Grande piscine, emprise du bâtiment | De `-47 224 -26` à `9 236 4` |
|
||||
| Volume d’eau du bassin | 50 × 25 blocs, quatre couches ; de `-44 225 -23` à `5 228 1`, avec les marches dans cette emprise |
|
||||
| Grande salle sèche, emprise | De `-54 228 8` à `-30 237 32`, niveau d’entrée 229 |
|
||||
| Parking, emprise | De `-8 116 8` à `22 237 32` |
|
||||
| Huit planchers du parking, niveaux Y | `117, 133, 149, 165, 181, 197, 213, 229` |
|
||||
| Bouche de sortie de l’égout | `-50 117 20` |
|
||||
|
||||
La piscine conserve ses 40 appuis, la grande salle 16, le parking 9 et la
|
||||
sortie d’égout 6. Ces présences sont constatées sur cette graine ; elles ne
|
||||
sont pas garanties sur toutes les îles.
|
||||
|
||||
### Fluides et connexions en création
|
||||
|
||||
L’essai observe **120 ticks du monde** après la mise en activité des chunks.
|
||||
Dix-neuf petits témoins temporaires fermés, placés dans le ciel, démontrent
|
||||
l’exécution réelle des fluides par une descente d’eau observée dans chaque
|
||||
chunk concerné. Ils sont retirés avant la sauvegarde et exclus des quantités
|
||||
d’eau des structures.
|
||||
|
||||
Après cette simulation, le grand bassin garde **4 976 cellules d’eau** dans
|
||||
son enceinte, et chaque égout conserve ses cinq cellules contrôlées. Les deux
|
||||
bouches gardent leur source et produisent de l’eau à l’extérieur. Leur écoulement
|
||||
rencontre cependant du terrain : premier bloc solide sous les sorties aux
|
||||
niveaux **113** et **191**. Ce témoin prouve des sorties d’eau actives, pas une
|
||||
chute libre continue jusqu’au vide.
|
||||
|
||||
Les chiffres de connexion suivants sont ceux de l’inspection initiale des
|
||||
pièces, avant la simulation des fluides. Le contrôle de réouverture ci-dessous complète ces premières mesures.
|
||||
|
||||
| États détaillés | Ville initiale | Expansion | Total |
|
||||
| --- | --- | --- | --- |
|
||||
| Vitres ou barreaux | 1 457 | 577 | 2 034 |
|
||||
| Clôtures | 36 | 14 | 50 |
|
||||
| Escaliers | 1 451 | 699 | 2 150 |
|
||||
| Comparaisons complètes au rendu | 2 944 | 1 290 | 4 234 |
|
||||
| Vérifications supplémentaires par règles natives | 2 277 | 1 276 | 3 553 |
|
||||
| Blocs exclus de cet oracle pour voisinage extérieur | 667 | 14 | 681 |
|
||||
| Raccords de vitres/barreaux à travers des frontières de chunks | 82 | 22 | 104 |
|
||||
|
||||
Les **681 exclusions** gardent leur comparaison d’état au rendu. Elles ne
|
||||
prouvent pas la stabilité de la connexion face au voisinage extérieur, comme
|
||||
pour les infrastructures. Aucun muret n’est représenté dans cette partie du
|
||||
contrôle urbain. Le bloc d’or témoin est placé en `-38 252 21` et le journal
|
||||
est sauvegardé avec l’empreinte
|
||||
`e776dcab879458b3268ad34f44a003cb53b8b03cfcb67378559d40732f8ce73f` ;
|
||||
ils sont conservés à la réouverture.
|
||||
|
||||
### Réouverture de la ville et raccords avec le terrain
|
||||
|
||||
La réouverture passe dans une seconde JVM (40687), avec **71 pièces natives**,
|
||||
**5 coffres différés**, **74 appuis**, **111 cibles accessibles**, le bloc d’or et
|
||||
le journal conservés. Reçu : `build/alpha20-evidence/city20-reload.json` ; journal
|
||||
`build/alpha20-city-reload-2.log`. La piscine et les sorties d’eau sont revérifiées.
|
||||
|
||||
Le test confronte les raccords au voisinage réellement présent. Au coin du
|
||||
parking `-8 118 8`, les grilles touchent de la deepslate au nord et à l’ouest :
|
||||
Minecraft ajoute légitimement ces raccords après une mise à jour de blocs.
|
||||
Le contrôle refuse tout changement de matériau, orientation, demi-bloc ou
|
||||
waterlogging ; seules les propriétés de raccord peuvent suivre les règles
|
||||
natives. À l’intérieur d’une géométrie connue, l’état du rendu reste exigé.
|
||||
|
||||
Sur les **4 234 blocs détaillés**, **4 178** sont alors des états stables selon
|
||||
l’oracle Minecraft ; **640** de ces états ont un voisinage extérieur ou un
|
||||
voisin ayant lui-même évolué. **56** autres gardent exactement leur état de
|
||||
plan et attendent une mise à jour native extérieure : ils sont comptés
|
||||
séparément, sans revendiquer leur stabilisation. Les **104 raccords entre
|
||||
chunks** restent présents. `externalNeighbors=696` compte les blocs dont le
|
||||
voisinage n’est plus entièrement identique au plan, dont les 640 + 56 cas ;
|
||||
ce compteur a donc une portée plus large que les 681 exclusions initiales.
|
||||
|
||||
## Compatibilité avec la version 19 publiée
|
||||
|
||||
Avec le binaire 20, les deux rapports natifs 19 sont **identiques intégralement**
|
||||
aux reçus publiés, en retirant seulement le PID et le chemin du monde de test.
|
||||
La comparaison porte sur plans, pièces, paramètres, parcours, supports, butins
|
||||
et modification témoin, à la création comme à la réouverture.
|
||||
|
||||
- Création canonique SHA-256 : `3b055a3bc0fc2ddf4146aef035f5103bb7fe20d55a86fa1d4b87aeedc02f7879`.
|
||||
- Réouverture canonique SHA-256 : `aed0962f8dfbfbbf69da1b2153861801c58a3ea2376e6bbec2ef44b90d555d63`.
|
||||
- Référence source publiée : `c23e72b8e6fd8308855459f26c08082a68e81b61`.
|
||||
- Reçus : `build/alpha20-evidence/legacy19-create.json` et `legacy19-reload.json`.
|
||||
|
||||
Les renderers, modèles et pièces historiques restent inchangés ; le preset 19
|
||||
caché est identique octet par octet au preset public de la version publiée.
|
||||
Les journaux refusent les chargements croisés 19/20 au lieu de convertir un monde.
|
||||
|
||||
## Option désactivée et petite île
|
||||
|
||||
Le scénario **Moyen, graine 0, option désactivée** passe : plan expérimental
|
||||
vide, aucun start natif expérimental et aucune entrée localisable. Le paramètre
|
||||
est enregistré. Reçu : `build/alpha20-evidence/experimental-off.json`.
|
||||
|
||||
Le témoin **Petit 512, graine 42, option activée** passe avec dix types de lieux,
|
||||
35 pièces de sites/voies et 50 chunks `FULL` contrôlés, réseaux oubliés compris.
|
||||
La cartothèque est présente, entrée **`-101 251 -25`**, et ses cinq cibles sont
|
||||
accessibles. Les **436 états détaillés** des infrastructures passent l’oracle
|
||||
natif, dont un avec voisin extérieur ; vingt raccords traversent des frontières
|
||||
de chunks. Reçu : `build/alpha20-evidence/small42-create.json`.
|
||||
|
||||
Les deux témoins 20 réunis rencontrent et parcourent les **quatorze types
|
||||
d’infrastructures**. Ils ne garantissent pas leur présence sur toutes les îles.
|
||||
La génération native 20 Grand n’est pas rejouée ici : ses réglages sont couverts
|
||||
par les contrôles de codec et les plans de placement conservés. Les six modèles
|
||||
urbains sont vérifiés dans les quatre orientations ; leur apparition naturelle
|
||||
est distinguée des fixtures ci-dessus.
|
||||
|
||||
## Prévisualisation et état de livraison
|
||||
|
||||
Le [montage du catalogue](architecture-alpha20.md) provient des exports de
|
||||
blocs des renderers et des modèles Minecraft. Il permet de comparer les
|
||||
silhouettes, matières, escaliers, vitres et corniches. Il ne montre ni le
|
||||
terrain final ni le rendu d’un client avec ses shaders ; ce n’est pas une
|
||||
capture de partie et il ne remplace pas les essais de circulation ou de lumière.
|
||||
|
||||
| Étape | État de ce reçu |
|
||||
| --- | --- |
|
||||
| Infrastructures et petits réseaux : création/réouverture natives | Réussi, dans le périmètre détaillé ci-dessus |
|
||||
| Ville 20, espaces publics et grands sous-sols : essais natifs | Création et réouverture réussies, 71 pièces et 5 coffres conservés |
|
||||
| Compatibilité avec le témoin 19 publié | Rapports complets identiques à la création et à la réouverture |
|
||||
| Autres configurations | Petit 512/graine 42 et option désactivée passent ; Grand 20 non rejoué nativement |
|
||||
| `check build assemblePack` final et intégrité des artefacts | Réussi ; huit GameTests généraux, JAR/MRpack/ZIP et hashes vérifiés |
|
||||
| Publication, canal packwiz et synchronisations Prism | Réussi ; deux passages isolés puis deux dans la même instance, 925 fichiers personnels et réglages préservés |
|
||||
|
||||
`./gradlew check build assemblePack --max-workers=1` passe en 4 min 39 s sur
|
||||
la machine de développement : 62 tâches, dont 55 exécutées, huit GameTests,
|
||||
125 chunks `FULL`, 5 923 blocs d’eau conservés et 1 800 ticks de fluides.
|
||||
Les tests des sept nouveaux composants 20 et des anciennes générations passent.
|
||||
Les réglages des trois tailles et leur sauvegarde passent également.
|
||||
Journal : `build/alpha20-check-build-pack.log`.
|
||||
|
||||
Le JAR de production ne contient aucune classe de test. Son identité est
|
||||
vérifiée dans le MRpack et le pack local ; les index et le bootstrap officiel
|
||||
Prism sont contrôlés. SHA-256 du JAR :
|
||||
`9ea10576e641d83c13e1277960a375448a22d416a98a6b327c4d6d2a204c423a`.
|
||||
Reçu : `build/alpha20-artifact-validation.log`.
|
||||
|
||||
Release immuable depuis `0edac29`, canal packwiz `4e0967d` et deux
|
||||
synchronisations isolées vérifiés. Les deux passages dans la même instance
|
||||
Prism conservent les hashes de 925 fichiers personnels et réglages. Un seul
|
||||
JAR20 est actif ; JAR19 et suivi packwiz sont sauvegardés avant remplacement.
|
||||
Voir le [reçu de distribution](packwiz.md#alpha20-publiée--10-septembre-2026).
|
||||
Aucun essai client visuel ni essai Windows natif n’a été effectué.
|
||||
@@ -1,140 +0,0 @@
|
||||
# Alpha.21 — contrat et vérification
|
||||
|
||||
Ticket WG-24, branche `codex/palettes-reseaux-alpha21`.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
|
||||
Java 25. Les dépendances restent celles de l’alpha.20.
|
||||
|
||||
## Résultat recherché
|
||||
|
||||
La ville reprend une palette de pierre lisse, pierre, briques de pierre,
|
||||
andésite et chêne. Les plans intérieurs sont conservés ; les corrections
|
||||
portent sur les matériaux, les appuis et les couvertures. Chaque infrastructure
|
||||
emploie un bois cohérent. Le tuf reste surtout au sol et dans les sous-sols.
|
||||
|
||||
La ville peut accueillir deux petits quartiers supplémentaires reliés à son
|
||||
cœur, avec du terrain naturel entre eux. La réserve centrale existante n’est
|
||||
pas agrandie. Les infrastructures reçoivent davantage de chemins entre des
|
||||
entrées réelles. Les recherches respectent des budgets de colonnes et refusent
|
||||
un quartier si sa liaison entière ne peut pas être construite.
|
||||
|
||||
Les galeries industrielles expérimentales ont cinq blocs de chaussée et un
|
||||
trottoir de chaque côté, des salles de maintenance et deux accès par escaliers.
|
||||
Le plan est recherché après les structures précédentes et évite leurs volumes,
|
||||
l’hydrologie. Un tablier continu peut franchir une cavité entre ses appuis ;
|
||||
les travées sans appuis complets sont limitées à 34 blocs, avec des piliers
|
||||
locaux d’au plus 16 blocs et deux couches de roche intacte sous les appuis. Chaque tronçon a une pièce native
|
||||
locale ; aucun démarrage de structure ne charge toute l’île.
|
||||
|
||||
## Sauvegardes et comparaison
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0**, avec
|
||||
**Structures expérimentales** activées. Petit 512, Moyen 724 et Grand 1024 restent
|
||||
les tailles proposées par le bouton natif **Personnaliser**.
|
||||
|
||||
Le nouveau codec est `sanctuary:island_v21`. Les codecs et pièces 20 et
|
||||
antérieurs restent disponibles, sans modifier leurs rendus. Le journal 21
|
||||
réutilise le schéma de persistance existant avec une génération distincte ;
|
||||
les ouvertures entre versions incompatibles sont refusées. Aucune conversion
|
||||
ni rénovation automatique des mondes précédents n’est effectuée.
|
||||
|
||||
Pour comparer sans les infrastructures, créer un autre monde avec l’option
|
||||
expérimentale désactivée. Une sauvegarde 21 nécessite le binaire 21 ou un futur
|
||||
binaire compatible ; elle ne se rétrograde pas en remplaçant simplement le JAR.
|
||||
|
||||
## Compatibilité vérifiée
|
||||
|
||||
373 fichiers de production historiques sont identiques au commit de publication
|
||||
20 (`0edac29`). Le preset caché `sanctuary_v20` est une copie exacte de son
|
||||
ancien preset public. Création et réouverture natives de la génération 20 donnent
|
||||
les mêmes plans, structures, appuis, accès et coffres que les rapports publiés.
|
||||
La comparaison ignore seulement l’identifiant du processus, le chemin du monde
|
||||
et deux nouveaux compteurs de détail ajoutés à l’oracle de connexion des tests.
|
||||
|
||||
## Galeries vérifiées dans Minecraft
|
||||
|
||||
Moyen, graine 0 : 23 modules dont deux accès et une salle, sur un axe de 340 blocs.
|
||||
Les entrées sont `-135 253 -178` et `-135 249 162` ; chaussée à Y 216. La portée
|
||||
sans appuis complets la plus longue du témoin est 17 blocs. Le plan emploie
|
||||
8 686 colonnes de bruit, avec un budget maximal de 12 000 ; cette recherche prend
|
||||
environ 11 secondes sur la machine de développement, hors reste du monde.
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent : 34 chunks
|
||||
FULL, 57 635 blocs vérifiés, mêmes pièces natives, mêmes coffres différés et
|
||||
modification témoin conservée. L’oracle de collision de Minecraft vérifie un
|
||||
corps de 0,6×1,8 bloc, 74 cibles et 520 pas entre les deux portiques ; il utilise
|
||||
les vrais escaliers et les demi-marches des trottoirs.
|
||||
|
||||
Une alcôve d’excavation inachevée ouvre le parement de la salle sur une paroi
|
||||
irrégulière de tuf, pierre et deepslate. Certains tronçons exposent de petits
|
||||
filons ordinaires. Le témoin contient cinq minerais de charbon, dix-neuf de
|
||||
fer, cinq de redstone, un de diamant et un d’émeraude ajoutés dans ces fronts.
|
||||
Cuivre, or et lapis font aussi partie des variantes possibles. Ce sont des
|
||||
blocs finis, sans mécanisme de réapparition après minage. Ces quantités excluent
|
||||
les minerais naturels de l’île et ne constituent pas une garantie par graine.
|
||||
|
||||
## Autres vérifications
|
||||
|
||||
La création et la réouverture natives de la ville passent. Le témoin possède
|
||||
un cœur et deux quartiers reliés, 65 pièces de rue, trois immeubles de logements,
|
||||
un hôtel, une bibliothèque, un jardin et une place. La réserve centrale n’est
|
||||
pas étendue. Les deux nouveaux chemins sont longs de 51 et 38 cellules. Une
|
||||
expansion de test distincte est également vérifiée ; aucune expansion personnelle
|
||||
n’est activée.
|
||||
|
||||
La piscine couverte, le garage, la salle d’expansion, leurs accès et les égouts
|
||||
restent présents. Les contrôles natifs vérifient les bassins et sorties après
|
||||
exécution observée de la simulation des fluides.
|
||||
|
||||
Les quatorze types de lieux patrimoniaux apparaissent sur Moyen, graine 0,
|
||||
avec douze liaisons et 127 segments. Leur création native passe : 141 pièces,
|
||||
91 271 blocs, 140 chunks FULL et quatorze coffres différés. Les trois réseaux
|
||||
oubliés sont également parcourus. Les chemins rejoignent deux entrées de ville.
|
||||
L’observatoire et le quai du vide restent isolés sur ce témoin : leurs chemins
|
||||
n’ont pas trouvé de tracé admissible dans le budget. La commande `/sanctuary structures` indique leurs entrées ; aucun raccord
|
||||
construit complet n’est revendiqué.
|
||||
|
||||
Les smokes des renderers, plans, pièces enregistrées et raccords passent.
|
||||
La réouverture des lieux patrimoniaux conserve également le plan, les pièces,
|
||||
les coffres et la modification témoin. L’option expérimentale désactivée passe :
|
||||
elle ne publie aucun module de galerie. `check build assemblePack` passe, avec les huit GameTests généraux. Le
|
||||
MRpack, son index et le JAR embarqué sont vérifiés. La release et le canal
|
||||
packwiz sont publiés. Deux synchronisations isolées puis deux dans la même
|
||||
instance Prism conservent les 925 fichiers personnels et réglages suivis.
|
||||
Voir le [reçu de distribution](packwiz.md#alpha21-publiée--10-septembre-2026).
|
||||
|
||||
Les chemins de quartier regroupent leurs pentes à l’intérieur de rectangles ;
|
||||
leurs frontières et raccords sont des paliers plats. Les structures restent
|
||||
conditionnées par le terrain admissible, dans un budget borné. La traversée
|
||||
industrielle ne relie pas encore tous les réseaux souterrains historiques.
|
||||
|
||||
Les vues de modèles éventuelles ne remplacent pas une inspection dans le client.
|
||||
Aucune validation Windows native ni charge multijoueur n’est revendiquée.
|
||||
|
||||
|
||||
## Reproduire les contrôles
|
||||
|
||||
Depuis ce dépôt, avec Java 25, les mondes de test restent sous `mods/sanctuary/build/`.
|
||||
Pour chaque scénario, exécuter la création puis sa réouverture avant tout autre
|
||||
scénario qui nettoie ce monde de développement :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTransit21Tests=true -PsanctuaryExpansionReload=true -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --max-workers=1
|
||||
```
|
||||
|
||||
Remplacer `sanctuaryTransit21Tests` par `sanctuaryLostCity21Tests` pour la ville
|
||||
ou `sanctuaryHeritage21Tests` pour les infrastructures. Le cas OFF ajoute
|
||||
`-PsanctuaryExperimentalStructures=false` à une création du scénario transit.
|
||||
Les rapports de cette livraison sont conservés localement dans
|
||||
`build/alpha21-evidence/`. `./gradlew check build assemblePack` constitue la
|
||||
vérification complète avant export.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
MRpack : `Sanctuary-0.1.0-alpha.21.mrpack`. Son JAR est identique au JAR du build.
|
||||
Les versions et hashes de l’index packwiz sont contrôlés, ainsi que le ZIP
|
||||
d’amorçage Prism. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
e528e42d99f9e1ea221f780ea089b49b42a1a4e9e24e4ddd001d045183202c21
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# Alpha.22 — placement sélectif
|
||||
|
||||
Ticket WG-25, branche `codex/placement-selectif-alpha22`.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3 et Java25,
|
||||
sans changement de dépendances.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les bâtiments de surface redeviennent sélectifs : une ville peut être absente
|
||||
si aucun emplacement n’est adéquat. Deux éléments de base sont prioritaires :
|
||||
**la grande traversée souterraine et la salle d’expansion**, même sans ville.
|
||||
Leurs emplacements sont réservés avant les petits réseaux et le catalogue
|
||||
expérimental. Une recherche bornée ne prouve pas qu’aucun autre emplacement
|
||||
possible n’existe dans toute l’île.
|
||||
|
||||
- Ville : aucune réserve artificielle de secours, aucun pilier de rattrapage,
|
||||
terrassement/remblai limité à deux blocs par colonne. Les fondations sont
|
||||
entièrement recertifiées dans la roche. Les quartiers supplémentaires exigent
|
||||
un chemin complet vers les quartiers retenus. Au plus trois propositions dans
|
||||
un budget partagé de20 000 colonnes et1 120 000 contrôles de volume.
|
||||
- Infrastructures : relief maximal de quatre blocs sur l’emprise17×17, coupe et
|
||||
remblai limités à deux blocs, deux couches de roche intacte sous chaque colonne
|
||||
de fondation, sans piliers de secours. Jusqu’à24 candidats par famille, avec
|
||||
les mêmes plafonds globaux de16 000 colonnes et800 000 sondages.
|
||||
- Chemins patrimoniaux : coupe/remblai limité à un bloc, accès indépendant et
|
||||
raccord complet requis pour publier une liaison.
|
||||
- Traversée : conserve le génie civil21, avec deux accès réels, une chaussée
|
||||
de cinq blocs et deux trottoirs. Les piliers sont bornés à16 blocs ; les
|
||||
travées sans grille complète sont limitées à34 blocs et reposent sur les
|
||||
modules adjacents. La recherche22 intervient avant les structures facultatives
|
||||
pour leur réserver l’emprise. Si les lignes droites sont refusées, une seconde
|
||||
passe cherche des accès différents et des détours. Chaque passe est limitée à
|
||||
12 000 colonnes et3 millions de sondages (24 000 et6 millions au total au plus).
|
||||
Ses parois peuvent traverser des cavités existantes sans écraser les volumes
|
||||
d’eau et de lave protégés.
|
||||
- Salle : recherche autonome d’un accès de surface et d’une salle25×25, sans
|
||||
faux quartier ni socle de ville. Une salle déjà présente sous une ville évite
|
||||
un doublon. Les égouts et annexes peuvent accompagner ce lieu indépendant.
|
||||
|
||||
|
||||
Les palettes, les plans intérieurs et les petits filons minables restent ceux
|
||||
de21. Les anciens petits réseaux expérimentaux17 et les sous-sols des villes gardent
|
||||
leur géométrie ; leurs protections restent respectées.
|
||||
Le nouveau principe narratif est documenté dans [la vision](vision.md) et
|
||||
[l’audit](structures-audit.md), sans activer de système de progression.
|
||||
|
||||
## Sauvegardes
|
||||
|
||||
Le seul preset public utilise désormais `sanctuary:island_v22`. Le preset caché
|
||||
`sanctuary_v21` est la copie de l’ancien preset public. Les anciens codecs,
|
||||
planificateurs et rendus restent disponibles. Les pièces21 sont réutilisées
|
||||
avec leur format existant ; les nouveaux coudes possèdent une pièce22 distincte
|
||||
pour fermer leurs murs sans modifier les jonctions21 sauvegardées.
|
||||
La salle indépendante possède un type de structure22
|
||||
et réutilise les pièces souterraines20.
|
||||
Le journal identifie la génération22 et refuse une lecture croisée avec21.
|
||||
Aucun monde personnel n’est ouvert, converti ou régénéré.
|
||||
|
||||
Les nouvelles règles nécessitent un **nouveau monde Sanctuary**. Petit512,
|
||||
Moyen724 par défaut et Grand1024 restent proposés. L’option **Structures
|
||||
expérimentales** reste enregistrée à la création ; en22 elle retire le catalogue
|
||||
facultatif, tandis que la traversée et la salle restent des éléments de base.
|
||||
Le commutateur Minecraft général des structures demeure respecté.
|
||||
|
||||
## Témoin natif Moyen, graine0
|
||||
|
||||
La création et la réouverture dans deux JVM distinctes passent. La ville est
|
||||
refusée ; la traversée comporte23 modules, dont deux accès et une salle de
|
||||
maintenance. Son axe s’étend sur340 blocs. Les accès sont `-170 253 -100` et
|
||||
`170 254 -100`. Le hall d’expansion indépendant se rejoint par la trappe
|
||||
`2 252 0` ; son réseau comprend l’accès, la salle, deux passages et un égout.
|
||||
La piscine est une annexe conditionnelle, absente sur ce témoin.
|
||||
|
||||
Les14 bâtiments patrimoniaux trouvent un terrain adapté ; sept liaisons sont
|
||||
retenues. Les115 chunks de l’emprise sont générés en statut FULL. Les plans,
|
||||
pièces natives et une modification témoin persistent après réouverture. Le
|
||||
contrôle natif de circulation vérifie la vraie trappe, les échelles soutenues,
|
||||
les ouvertures et le trajet jusqu’au socle central. Aucun déplacement de joueur
|
||||
ou chargement d’un monde personnel n’a été utilisé pour ces essais.
|
||||
|
||||
## Vérifications complémentaires
|
||||
|
||||
**Petit512, graine42 :**17 modules, dont un coude sauvegardé avec sa pièce22,
|
||||
et une salle indépendante. Les anciens alignements droits sont refusés ; le
|
||||
repli trouve une paire d’accès naturels et conserve une chaussée continue.
|
||||
Les portiques sont `-68 250 -93` et `68 250 43`, la trappe du hall `43 244 -10`.
|
||||
La marche native relie les sorties en564 déplacements et atteint53 points de
|
||||
contrôle. Huit bâtiments patrimoniaux et six liaisons sont admis ; aucune ville
|
||||
n’est forcée. Les88 chunks de l’emprise sont vérifiés en statut FULL.
|
||||
|
||||
**Grand1024, graine42 :** un quartier naturel est admis avec son réseau de
|
||||
sous-sols : salle d’expansion, piscine, égout et parking. Aucun hall autonome
|
||||
supplémentaire n’est créé. La traversée comporte31 modules et s’étend sur476
|
||||
blocs ; portiques `-272 249 -8` et `204 250 -8`. La marche native les relie
|
||||
en740 déplacements et atteint98 points de contrôle. Quatorze bâtiments
|
||||
patrimoniaux et huit liaisons sont admis. Les179 chunks de l’emprise sont
|
||||
vérifiés en statut FULL ; la salle et ses17 ouvertures sont accessibles.
|
||||
|
||||
Les tests purs des trois tailles passent sur des terrains plats adaptés ;
|
||||
falaises, eau/protections, vides et appuis insuffisants sont refusés pour les
|
||||
bâtiments. Le hall autonome et ses annexes sont vérifiés sans fixture de ville.
|
||||
Les suites historiques restent actives. Avec l’option expérimentale désactivée,
|
||||
le témoin Moyen conserve ses23 modules de traversée et sa salle indépendante,
|
||||
sans bâtiments patrimoniaux ni petits réseaux expérimentaux. La marche native
|
||||
relie les deux sorties en548 déplacements et atteint74 points de contrôle ;
|
||||
les échelles et l’accès au hall restent praticables.
|
||||
|
||||
Les créations et réouvertures de génération21 sont identiques aux rapports de
|
||||
la release21, hors identifiants de processus et dossier de test. Les391 fichiers
|
||||
de production historiques qui ne nécessitent pas de raccord22 restent identiques
|
||||
octet pour octet ; le preset caché21 est la copie exacte du précédent preset
|
||||
public. Les masques sont vérifiés à l’intérieur et hors des structures.
|
||||
|
||||
`check build assemblePack` passe :75 tâches, dont les huit GameTests généraux.
|
||||
Le JAR, l’index packwiz, le MRpack et le ZIP Prism sont vérifiés. Aucun code de
|
||||
test n’est embarqué dans le JAR. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
10d7ef32ac2e81a977ea4854503bafa29e0bc10929984b76df01d6f5f61f9056
|
||||
```
|
||||
|
||||
SHA-256 du MRpack :
|
||||
|
||||
```text
|
||||
0b553fcdd8b6a2ec26f72059e428d2cc0ce526f36ef8580dd3e50c1dfa40ec78
|
||||
```
|
||||
|
||||
Les reçus de publication et de synchronisation sont suivis dans
|
||||
[la distribution](packwiz.md).
|
||||
|
||||
Ces témoins vérifient les trois tailles, avec et sans ville ; ils ne constituent
|
||||
pas une preuve de présence pour toutes les graines. La recherche reste bornée
|
||||
et refuse un volume protégé ou insuffisamment porté. Aucun essai visuel client
|
||||
ou essai Windows natif n’est revendiqué.
|
||||
|
||||
## Reproduire dans le dossier de développement
|
||||
|
||||
Avec Java25, création du témoin Moyen puis réouverture du même monde de test :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySelective22Tests=true -PsanctuaryDensityMaps=false -PsanctuaryExpansionReload=true
|
||||
```
|
||||
|
||||
Pour une nouvelle création Petit ou Grand, ajouter `-PsanctuaryTestSeed=42`
|
||||
et `-PsanctuaryTestDiameter=512` ou `1024`. Le cas OFF utilise
|
||||
`-PsanctuaryExperimentalStructures=false`. Chaque réouverture doit suivre la
|
||||
création correspondante, avec les mêmes paramètres. Les rapports sont dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/selective22-<graine>-<diamètre>/`.
|
||||
|
||||
Le témoin historique utilise `-PsanctuaryHeritage21Tests=true`, puis le même
|
||||
drapeau avec `-PsanctuaryExpansionReload=true`. La vérification de livraison
|
||||
reste `./gradlew check build assemblePack`. Exécuter ces JVM séquentiellement.
|
||||
@@ -1,433 +0,0 @@
|
||||
# Validation alpha.23.1
|
||||
|
||||
Cible : Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API
|
||||
0.160.0+26.3, Java 25. Branche `codex/natural-secrets-alpha23`, ticket WG-27.
|
||||
|
||||
**Livraison locale vérifiée le 11 septembre 2026.** Les neuf cas de la matrice
|
||||
initiale, les structures natives et leurs réouvertures, les océans échantillonnés
|
||||
et le cycle réel d’expansion passent. `check build`, `assemblePack` et le MRpack
|
||||
sont vérifiés. **Publication et synchronisation Prism restent bloquées par
|
||||
l’authentification Git indisponible.** Le [contrat de génération](generation-alpha23.1.md)
|
||||
décrit le périmètre ; les limites de chaque preuve sont conservées ci-dessous.
|
||||
|
||||
## Environnement et preuves
|
||||
|
||||
Les essais utilisent un serveur Fabric GameTest sans interface graphique sur
|
||||
Mac, dans des mondes de développement ignorés. Aucun monde personnel n’a été
|
||||
ouvert ou modifié. **Windows n’a pas été testé ; aucune validation visuelle
|
||||
client, avec ou sans shaders, n’a été effectuée.**
|
||||
|
||||
Les preuves locales sont dans `build/alpha231-evidence/matrix.json` et les
|
||||
rapports `secrets-<graine>-<diamètre>.json` du même dossier. Pour le Moyen,
|
||||
graine 0, les rapports finaux `final-medium.json` et `final-medium-reload.json`
|
||||
remplacent le résultat intermédiaire de `secrets-0-724.json` ; leurs durées
|
||||
figurent dans `postchecks.json`. Les réouvertures possèdent un rapport
|
||||
`*-reload.json`. Ces fichiers de développement ne sont
|
||||
pas des artefacts publics versionnés. Les journaux correspondants sont indiqués
|
||||
dans la matrice ; les assertions sont dans
|
||||
[Secrets23WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Secrets23WorldGameTests.java).
|
||||
|
||||
Les premières exécutions précèdent la dernière optimisation du préfiltre
|
||||
minéral. La distinction est conservée ci-dessous, avec la reprise du Moyen,
|
||||
graine 0, et la réouverture du Petit, graine 42, sur le préfiltre final.
|
||||
|
||||
## Créations réellement vérifiées en FULL
|
||||
|
||||
Les neuf cas ci-dessous ont `passed: true` et un code de sortie nul. Dans ces
|
||||
témoins, aucun quartier Lost City n’est demandé sur l’île initiale ; un
|
||||
observatoire et un hall indépendant sont toujours présents. Le réseau d’atelier
|
||||
caché apparaît dans huit cas sur neuf : il n’est pas retenu dans le Petit,
|
||||
graine 0. Lorsqu’il est présent, il ne contient que son accès, un corridor et
|
||||
l’atelier. Le hall n’inclut aucune piscine. Les structures expérimentales sont
|
||||
activées.
|
||||
|
||||
| Graine | Taille nominale | Chunks FULL contrôlés | Modules de traversée | Liaisons patrimoniales | Rails natifs vérifiés | Sanctuaire minéral | État |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | --- | --- |
|
||||
| 0 | Petit 512 | 88 | 19 | 2 | 85 | Présent | Réussi avec le préfiltre final ; atelier caché non retenu |
|
||||
| 0 | Moyen 724 | 85 | 23 | 2 | 68 | Présent | Création et réouverture finales réussies (`final-medium*.json`) |
|
||||
| 0 | Grand 1 024 | 105 | 35 | 2 | 73 | Présent | Réussi avec le préfiltre final |
|
||||
| 42 | Petit 512 | 88 | 17 | 1 | 23 | Présent | Création réussie ; réouverture avec le préfiltre final réussie |
|
||||
| 42 | Moyen 724 | 82 | 23 | 3 | 135 | Présent | Réussi avec le préfiltre final |
|
||||
| 42 | Grand 1 024 | 125 | 31 | 1 | 43 | Présent | Réussi avec le préfiltre final |
|
||||
| 2026 | Petit 512 | 85 | 17 | 2 | 36 | Présent | Réussi avec le préfiltre final |
|
||||
| 2026 | Moyen 724 | 79 | 23 | 1 | 54 | Présent | Réussi avec le préfiltre final |
|
||||
| 2026 | Grand 1 024 | 107 | 31 | 2 | 58 | Présent | Réussi avec le préfiltre final |
|
||||
|
||||
Le nombre de chunks contrôlés correspond aux emprises des structures retenues,
|
||||
pas à toute l’île. Ces neuf témoins ne garantissent pas le même résultat sur
|
||||
toutes les graines.
|
||||
Le nombre de liaisons ne signifie pas que tous les lieux sont reliés : les
|
||||
tronçons ferroviaires locaux peuvent subsister quand un raccord lointain est
|
||||
refusé par le terrain.
|
||||
|
||||
Les durées du runner consignées pour les neuf créations retenues vont de
|
||||
68,38 à 148,52 secondes. Elles incluent le lancement de la JVM, la
|
||||
préparation du monde, la génération FULL des emprises et les assertions.
|
||||
|
||||
| Graine | Petit 512 | Moyen 724 | Grand 1 024 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 0 | 78,86 s | 96,19 s | 148,52 s |
|
||||
| 42 | 68,38 s | 79,60 s | 113,23 s |
|
||||
| 2026 | 70,20 s | 89,04 s | 133,05 s |
|
||||
|
||||
Le Moyen, graine 0, utilise ici la durée de sa création finale dans
|
||||
`postchecks.json`, et non les 99,07 secondes de son ancien essai. Sa réouverture
|
||||
finale prend 57,24 secondes.
|
||||
|
||||
**Ce ne sont ni des temps de rendu client, ni un benchmark Windows, ni une
|
||||
mesure du gain de performance par rapport à l’alpha.22.**
|
||||
|
||||
## Ce que le GameTest contrôle
|
||||
|
||||
- Chaque emplacement accepté possède un vrai départ de structure natif. Les
|
||||
pièces sauvegardées retrouvent les mêmes données après sérialisation puis
|
||||
lecture ; leurs emprises sont chargées jusqu’au statut FULL.
|
||||
- La traversée est parcourue par un contrôle des collisions natives pour un
|
||||
volume de joueur de 0,6 × 1,8 bloc. Les montées souterraines exigent un escalier
|
||||
orienté ; les abords naturels des portiques peuvent comporter un saut normal
|
||||
d’un bloc. La trappe, les échelles et les ouvertures du hall sont contrôlées.
|
||||
- Toutes les cellules marquées comme portant un rail possèdent réellement un
|
||||
bloc de rail après décoration, avec un ballast solide dessous. Les lacunes
|
||||
prévues restent réparables ; aucun trajet réel en wagon n’a été testé.
|
||||
- Le centre du sanctuaire minéral est contrôlé par échantillonnage dans les
|
||||
chunks FULL : sol solide et deux blocs d’air au-dessus. Le test pur vérifie
|
||||
aussi l’intégralité du centre de 39 × 39 et les quatre orientations.
|
||||
- Les voies de surface préservent le disque naturel d’arrivée de 48 blocs de
|
||||
rayon. Les volumes retenus ne nécessitent aucune ville de surface.
|
||||
|
||||
Ces contrôles ne constituent pas une appréciation esthétique des ruines ou des
|
||||
cavités. La présence de géométrie et la circulation vérifiée ne valident pas
|
||||
encore l’équilibrage des ressources, l’expérience multijoueur ou les effets
|
||||
visuels de l’eau et de la lumière.
|
||||
|
||||
## Repères de visite — graine 42
|
||||
|
||||
Ces coordonnées proviennent des géométries des rapports. Elles s’appliquent à
|
||||
un **nouveau monde de génération 23**, avec les structures et les secrets
|
||||
expérimentaux activés, dans la taille correspondante. Ce sont des repères de
|
||||
développement ; aucune téléportation dans un monde personnel n’a été effectuée.
|
||||
|
||||
| Lieu ou accès, coordonnées X Y Z | Petit 512 | Moyen 724 | Grand 1 024 |
|
||||
| --- | --- | --- | --- |
|
||||
| Entrée de l’observatoire | `54 243 -40` | `186 251 -110` | `204 250 -121` |
|
||||
| Accès de l’atelier caché | `-45 244 -131` | `-116 250 -157` | `-174 251 -128` |
|
||||
| Trappe du hall indépendant | `32 168 -72` | `-97 223 -53` | `46 256 146` |
|
||||
| Premier portique parcouru | `-68 250 -93` | `-170 256 82` | `-272 249 -8` |
|
||||
| Second portique parcouru | `68 250 43` | `170 252 82` | `204 250 -8` |
|
||||
| Point de localisation intérieur du sanctuaire minéral | `-144 165 -61` | `-121 208 -23` | `-103 144 194` |
|
||||
| Coin minimal de son enveloppe | `-183 161 -97` | `-160 204 -59` | `-142 140 158` |
|
||||
|
||||
L’enveloppe minérale conserve 79 × 73 × 27 blocs — largeur, profondeur, hauteur —
|
||||
et un centre dégagé de 39 × 39. Une ouverture sur un flanc exposé peut donner
|
||||
sur le vide ; les essais ne garantissent pas un chemin extérieur déjà construit
|
||||
jusqu’à elle. Le point de localisation reste dans la salle.
|
||||
|
||||
## Réouverture et limites de l’admission minérale
|
||||
|
||||
Le Petit, graine 42, passe sa réouverture dans une JVM distincte : processus
|
||||
7581 pour la création et 7664 pour la lecture. Les paramètres du générateur,
|
||||
les plans, les départs natifs et la modification témoin de l’observatoire sont
|
||||
conservés. Le sanctuaire reste présent, avec 23 rails natifs dans le monde.
|
||||
La création précédait le préfiltre final ; sa réouverture, avec ce préfiltre,
|
||||
conserve la même géométrie. Ce cas ne valide pas toutes les sauvegardes.
|
||||
|
||||
Le Moyen, graine 0, passe sa création et sa réouverture finales dans deux JVM
|
||||
distinctes, processus 8650 puis 8777 (`final-medium.json` et
|
||||
`final-medium-reload.json`). Le sanctuaire est présent, avec 85 chunks FULL
|
||||
contrôlés et 68 rails natifs. Les paramètres du générateur, les géométries,
|
||||
les départs natifs et la modification témoin sauvegardée sont identiques entre
|
||||
les deux rapports. Le point de localisation intérieur du sanctuaire est
|
||||
`-93 143 81`, dans une enveloppe dont le coin minimal est `-132 139 45`.
|
||||
Ce résultat final remplace le refus minéral de la recherche intermédiaire.
|
||||
|
||||
La recherche du sanctuaire reste bornée : 48 positions, quatre orientations,
|
||||
trois profondeurs, au plus 10 000 colonnes et 1 200 000 sondages. Le préfiltre
|
||||
examine une grille avant la certification détaillée. Celle-ci conserve au moins
|
||||
60 % de couverture et d’appuis directs sur deux blocs de roche native ; chaque
|
||||
colonne sans appui direct doit être à dix blocs au plus d’un appui. Le volume
|
||||
protégé est vérifié intégralement. Le rendu ne pose aucun pilier hors de son
|
||||
enveloppe pour sauver un mauvais emplacement.
|
||||
|
||||
Les diagnostics distinguent ouverture obstruée, couverture ou roche
|
||||
insuffisantes, appuis trop éloignés, volumes protégés et budget épuisé. Un refus
|
||||
ne prouve pas qu’aucun emplacement imaginable n’existe ailleurs dans l’île.
|
||||
Les témoins obtenus ne garantissent donc pas la présence du sanctuaire pour
|
||||
toutes les graines. Les règles des autres structures n’ont pas été assouplies
|
||||
pour faire accepter ce lieu.
|
||||
|
||||
## Autres vérifications
|
||||
|
||||
### Réalisées
|
||||
|
||||
Le smoke ciblé `secrets23Smoke` a passé ses témoins de géométrie, sauvegarde et
|
||||
refus ; `structureCensusSmoke` possède également une réussite dans le journal
|
||||
local `build/alpha23.1-corrected-smokes.log`.
|
||||
|
||||
La suite générale réussit ses **24 GameTests obligatoires** dans
|
||||
`build/alpha23.1-general.log` : 51,84 secondes pour l’exécution des tests et
|
||||
117,75 secondes pour le runner complet. La dernière entrée `general` de
|
||||
`postchecks.json` possède un code de sortie nul. Elle remplace le premier
|
||||
essai interrompu par l’ancienne assertion du nombre de biomes, depuis
|
||||
actualisée. Les suites spécialisées et les commandes finales sont attestées
|
||||
séparément dans les sections suivantes.
|
||||
|
||||
Les deux modes OFF passent dans les mondes de développement de graine 0,
|
||||
taille Moyen 724. Les reçus et les durées se trouvent dans
|
||||
`build/alpha231-evidence/` et `postchecks.json` :
|
||||
|
||||
| Option | Preuve | Résultat contrôlé | Durée du runner |
|
||||
| --- | --- | --- | ---: |
|
||||
| `sanctuaryExperimentalStructures=false` | `optional-off.json` | Aucun observatoire, atelier, sanctuaire minéral ni rail patrimonial ; les 23 modules de traversée et le hall indépendant restent présents. 43 chunks FULL contrôlés, parcours de la traversée et accès du hall réussis. | 69,04 s |
|
||||
| `sanctuaryGenerateStructures=false` | `global-off.json` | Aucun départ natif valide dans les trois chunks contrôlés ; 293 376 positions vérifiées sans les rails et blocs manufacturés ciblés par le test. | 30,50 s |
|
||||
|
||||
Ces essais attestent les comportements observés dans leurs emprises ; le
|
||||
contrôle global OFF n’est pas un inventaire de tous les chunks de l’île.
|
||||
|
||||
La création puis la réouverture du générateur historique
|
||||
`sanctuary:island_v22` passent également pour la graine 0, taille Moyen 724,
|
||||
avec les structures expérimentales activées. Les rapports `legacy22.json`
|
||||
et `legacy22-reload.json` proviennent de JVM distinctes, processus 9088 puis
|
||||
9208, en 130,05 et 86,11 secondes. Ils contrôlent 115 chunks FULL et conservent
|
||||
14 sites patrimoniaux, sept liaisons, 23 modules de traversée, un hall indépendant
|
||||
et la modification témoin sauvegardée.
|
||||
|
||||
`legacy22-comparison.json` compare ces résultats aux rapports alpha.22 de
|
||||
référence `build/alpha22-evidence/native22-create.json` et
|
||||
`native22-reload.json`. Tous les champs de référence restent identiques, à
|
||||
l’exception des identifiants de processus et chemins de mondes, volontairement
|
||||
exclus. Les seules clés supplémentaires autorisées dans les nouveaux rapports
|
||||
sont `transit_native_walk` et `hall_native_access` : ce sont des diagnostics
|
||||
de parcours ajoutés au test, pas une modification du format de sauvegarde.
|
||||
Ils passent aussi : les deux portiques sont reliés par un parcours de 548
|
||||
pas de contrôle et le hall dispose d’un accès utilisable avec 22 blocs d’échelle
|
||||
et huit ouvertures libres.
|
||||
|
||||
Le même reçu atteste que le preset conservé `sanctuary_v22.json` est identique
|
||||
octet par octet à l’ancien preset `sanctuary.json` du commit `e654d56`.
|
||||
Cette comparaison porte sur ce témoin de génération 22 créé et rechargé avec
|
||||
le code actuel ; elle ne constitue ni une migration ni un essai de tous les
|
||||
anciens mondes personnels.
|
||||
|
||||
### Réservations réelles d’expansion
|
||||
|
||||
Quatre appels réels à `ExpansionRuntime.Session.create` passent dans des JVM
|
||||
neuves, sans aperçu d’expansion avant l’appel, sur des mondes de développement
|
||||
de graine 0 et de génération 23. Chaque essai réussit dès le premier identifiant
|
||||
`admission_23_0`, conserve le délai de recherche de production de 30 secondes
|
||||
et termine avec une seule nouvelle île au statut `reserved`.
|
||||
|
||||
Les preuves sont dans `build/alpha231-evidence/admission-costs.json` et les
|
||||
quatre rapports ci-dessous. La durée d’admission est celle de l’appel à
|
||||
`Session.create` ; la durée du runner inclut le lancement et les contrôles.
|
||||
|
||||
| Demande | Preuve | Admission | Runner complet |
|
||||
| --- | --- | ---: | ---: |
|
||||
| 512, tempéré, naturel | `reserve-512-temperate-natural.json` | 206 ms | 53,62 s |
|
||||
| 1 024, tempéré, naturel | `reserve-1024-temperate-natural.json` | 698 ms | 52,33 s |
|
||||
| 1 024, tempéré, océan | `reserve-1024-temperate-ocean.json` | 441 ms | 53,40 s |
|
||||
| 1 024, Lost City, naturel | `reserve-1024-lost_city-natural.json` | 12 849 ms | 64,15 s |
|
||||
|
||||
Le contrôle vérifie la garantie native appliquée par `Session.create`, la
|
||||
relecture du journal durable, la conservation du profil capturé avant l’appel
|
||||
et l’idempotence : répéter le même identifiant ne crée pas une seconde
|
||||
réservation et ne réécrit pas le journal. Le profil initial de priorités est
|
||||
vide dans ces quatre témoins ; ils ne couvrent pas encore son évolution après
|
||||
le recensement d’un continent généré.
|
||||
|
||||
Pour Lost City, le journal
|
||||
`build/alpha23.1-reserve-1024-lost_city-natural.log` atteste un plan accepté
|
||||
comportant un quartier et un réseau souterrain ; cette planification prend
|
||||
12 575 ms à l’intérieur de l’appel d’admission.
|
||||
|
||||
**Ces quatre essais sont des réservations uniquement.** Leurs reçus indiquent
|
||||
`reserve_only: true` et `full_generation_checked: false`. Ils ne valident pas
|
||||
la préparation complète, le passage à `ready`, les blocs posés en FULL ou leur
|
||||
réouverture ; leurs durées ne sont donc pas des temps de génération de
|
||||
continents. Aucune publication ni expansion dans un monde personnel n’est
|
||||
attestée par ces essais.
|
||||
|
||||
### Cycle complet d’une expansion réelle
|
||||
|
||||
`build/alpha231-evidence/admission.json` atteste le passage complet de
|
||||
`Session.create` à `ready`, processus 11335, sur un monde de graine 0 :
|
||||
expansion tempérée naturelle de **256 blocs**, identifiant `admission_23_0`,
|
||||
centre `2096 -160`, graine d’île `-6422943410799146666`. Cette taille est
|
||||
distincte des réservations de 512 et 1 024 blocs mesurées plus haut.
|
||||
|
||||
La première demande est réservée en 1 034 ms sous le délai de 30 secondes.
|
||||
La préparation prend 94 780 ms ; le journal `build/alpha23.1-admission.log`
|
||||
confirme **418 chunks FULL** lorsque l’expansion devient prête. Le runner
|
||||
complet termine avec un code de sortie nul en 160,01 secondes.
|
||||
|
||||
Un portail en ruine natif est effectivement généré au départ de chunk
|
||||
`131 -10`. Le contrôle vérifie la couverture complète du recensement, la
|
||||
présence de ce départ natif, le journal durable et l’idempotence avant et après
|
||||
la préparation. Le profil capturé pour cette réservation reste vide ; après
|
||||
génération, le recensement propose douze familles prioritaires pour les
|
||||
recherches suivantes, sans réécrire le profil de l’île déjà créée.
|
||||
|
||||
Le reçu indique `reserve_only: false`, `census_verified: true`,
|
||||
`idempotent: true` et `passed: true`. Ce terrain distant ne nécessite pas de
|
||||
certificat de réutilisation de chunks du vide.
|
||||
|
||||
`admission-reload.json` confirme la réouverture réussie dans le processus
|
||||
11614, distinct du processus 11335 de création, en 56,07 secondes de runner.
|
||||
L’île est immédiatement retrouvée à l’état `ready`, sans nouvel appel de
|
||||
création (`create_called: false`) ni redémarrage de sa préparation. Le test retrouve le journal à
|
||||
l’identique, le descripteur et son profil, le départ natif et ses références,
|
||||
ainsi que le recensement. La preuve reste limitée à ce cycle tempéré naturel
|
||||
de 256 blocs ; les grandes réservations ne deviennent pas pour autant des
|
||||
témoins FULL.
|
||||
|
||||
### Catalogue et structures natives en FULL
|
||||
|
||||
`build/alpha231-evidence/native-create.json` atteste la réussite de la création
|
||||
native, processus 10548 : **206 chunks FULL contrôlés**, sur cinq témoins de
|
||||
structures. La dernière entrée `native-create` de `postchecks.json` possède
|
||||
un code de sortie nul, en 232,40 secondes de runner. Les assertions sont dans
|
||||
[Native23WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Native23WorldGameTests.java).
|
||||
Les témoins utilisent des îles et des priorités choisies pour exercer ces
|
||||
structures ; ils ne constituent pas une mesure de leur fréquence naturelle.
|
||||
|
||||
Le catalogue contient exactement les **52 identifiants distincts** du registre
|
||||
Minecraft 26.3-pre-2. Parmi eux, 46 sont classés admissibles dans l’Overworld ;
|
||||
la cité ancienne reste réservée aux Cavernes, quatre entrées au Nether et la
|
||||
cité de l’End à l’End. Cette concordance du catalogue ne signifie pas que les
|
||||
52 structures ont été générées en FULL dans cet essai.
|
||||
|
||||
La vérification d’admission est rejouée avec le descripteur Lost City du témoin
|
||||
de réservation précédent : le quartier et le réseau souterrain existent dans
|
||||
le plan avant et après son insertion dans la liste des îles du générateur.
|
||||
Le placement natif reste accepté lorsque leurs volumes de construction sont
|
||||
pris en compte. Ce contrôle porte sur une ville réellement planifiée,
|
||||
pas sur un plan vide ; il ne valide pas encore son quartier entier en FULL.
|
||||
|
||||
| Témoin FULL | Présences contrôlées après décoration |
|
||||
| --- | --- |
|
||||
| Forteresse (`stronghold`) | Salle de portail native, 12 cadres de portail de l’End, générateur de monstres et coffres. |
|
||||
| Chambres des épreuves (`trial_chambers`) | Générateurs d’épreuves et coffres-forts natifs (`vault`). |
|
||||
| Monument océanique | Salles immergées en prismarine, eau et trois grands gardiens chargés dans le monde ; eau de surface et roche sous le fond vérifiées au centre de l’île témoin. |
|
||||
| Igloo | Neige, coffre du sous-sol natif et échelles de son puits. |
|
||||
| Pyramide du désert | Grès, quatre coffres et neuf TNT du piège natif. |
|
||||
|
||||
Les compteurs bruts `full_blocks` parcourent les emprises des pièces, qui
|
||||
peuvent se recouvrir : leurs valeurs ne forment pas un inventaire de blocs
|
||||
uniques. Ainsi, la valeur 91 pour `vault` n’atteste pas 91 coffres-forts
|
||||
distincts. Le tableau rapporte surtout les présences requises. Aucun combat,
|
||||
déclenchement d’épreuve, ouverture de butin ou usage du portail n’a été joué.
|
||||
|
||||
Les départs gardent leur identité dans le registre, leur géométrie admise et
|
||||
leurs données après sérialisation et lecture. Le monument est adapté au fond
|
||||
océanique et son agencement intérieur se reconstruit avec la graine de l’île.
|
||||
Pour les chambres des épreuves, seule la hauteur absolue de départ est adaptée :
|
||||
les pools, processeurs, alias et contenus natifs restent identiques.
|
||||
|
||||
La localisation par les tags natifs de l’œil de l’Ender et des cartes de
|
||||
monument, de chambres des épreuves et de pyramide trouve les vrais départs.
|
||||
Une sélection mélangeant plusieurs types vanilla réussit également. La
|
||||
recherche en lecture seule conserve les compteurs de références ; avec
|
||||
`skipReferenced`, seul le départ gagnant reçoit une référence supplémentaire.
|
||||
Ces recherches ne changent pas la liste des îles.
|
||||
|
||||
La présence des trois grands gardiens est vérifiée après chargement effectif
|
||||
des entités des chunks du monument. Elle ne repose pas uniquement sur leur
|
||||
NBT sauvegardé. La réouverture réussit aussi : `native-reload.json`, processus
|
||||
10722, contrôle de nouveau 206 chunks FULL. Les données `full_tags` des cinq
|
||||
départs, incluant leurs références, et les compteurs `full_blocks` sont
|
||||
identiques entre création et réouverture ; les trois grands gardiens sont
|
||||
présents après rechargement des entités. Cette preuve reste limitée aux cinq
|
||||
témoins retenus.
|
||||
|
||||
### Six profils océaniques en FULL
|
||||
|
||||
`build/alpha231-evidence/ocean.json` atteste la réussite de la matrice
|
||||
océanique, processus 11735, graine de monde 0, génération 23 : **34 chunks
|
||||
échantillons demandés au statut FULL** pour six îles temporaires. Le runner
|
||||
termine en 91,39 secondes, avec un code de sortie nul. Cette sélection couvre
|
||||
un bassin profond, une rive et l’extérieur de chaque profil, ainsi que les
|
||||
chunks voisins nécessaires aux témoins de glace ; elle ne génère pas la
|
||||
totalité des six océans.
|
||||
|
||||
| Diamètre | Climat | Graine d’île | Eau à Y | Fond au point témoin | Biome profond stocké |
|
||||
| --- | --- | ---: | ---: | ---: | --- |
|
||||
| 512 | Glacial | 23 042 | 220 | 176 | `minecraft:deep_frozen_ocean` |
|
||||
| 512 | Tempéré | 23 143 | 220 | 171 | `minecraft:deep_ocean` |
|
||||
| 512 | Chaud | 23 244 | 220 | 176 | `minecraft:deep_lukewarm_ocean` |
|
||||
| 1 024 | Glacial | 23 345 | 241 | 193 | `minecraft:deep_frozen_ocean` |
|
||||
| 1 024 | Tempéré | 23 446 | 241 | 195 | `minecraft:deep_ocean` |
|
||||
| 1 024 | Chaud | 23 547 | 241 | 198 | `minecraft:deep_lukewarm_ocean` |
|
||||
|
||||
Chaque profil conserve 16 appuis rocheux sur 16 et 16 échantillons immergés
|
||||
sur 16, après décoration native. Les rives stockent `minecraft:snowy_beach`
|
||||
pour les deux profils glaciaux et `minecraft:beach` pour les autres. Les
|
||||
points extérieurs contrôlés restent entièrement dans l’air et le biome
|
||||
`minecraft:the_void`.
|
||||
|
||||
Les deux profils glaciaux passent également un placement explicite des
|
||||
features natives d’iceberg et de glace bleue. Avec la graine de feature
|
||||
23 041, les sommets de glace atteignent respectivement Y 223 et Y 244,
|
||||
au-dessus de leur niveau marin local. Le contexte de décoration de production
|
||||
fournit ce niveau au générateur et à la région, conserve la protection du fond,
|
||||
puis restaure le niveau global à −64. Ces placements explicites contrôlent
|
||||
la compatibilité des features ; ils n’attestent pas une fréquence naturelle
|
||||
d’icebergs dans toutes les graines.
|
||||
|
||||
L’ancien échec `ocean-ice-failure.json` utilisait un tirage d’iceberg pouvant
|
||||
ne poser aucune glace dans le code vanilla. Le nouveau témoin déterministe
|
||||
remplace ce tirage ; `ocean.json` constitue la preuve finale de cette suite.
|
||||
La liste initiale des îles et les fichiers du journal sont inchangés après
|
||||
les essais (`journal_unchanged: true`). **La réouverture des six profils dans
|
||||
une autre JVM n’a pas été exécutée** ; le monument océanique dispose, lui,
|
||||
de la preuve de réouverture décrite plus haut.
|
||||
|
||||
### Fabrication et distribution
|
||||
|
||||
`./gradlew check build` réussit en 3 min 48 s : 80 tâches, dont 73 exécutées.
|
||||
Ses **24 GameTests** passent en 51,12 s, avec les régressions historiques,
|
||||
le Blocodex, les priorités, le journal, le recensement et les protections.
|
||||
`./gradlew assemblePack` réussit en 3 min 45 s : 81 tâches, dont 70 exécutées.
|
||||
Cette seconde commande relance ses dépendances de vérification ; aucun échec.
|
||||
Journaux : `build/alpha23.1-check-build.log` et
|
||||
`build/alpha23.1-assemble-pack.log`.
|
||||
|
||||
L’export packwiz produit `build/Sanctuary-0.1.0-alpha.23.1.mrpack`.
|
||||
Le contrôle des artefacts vérifie les versions, le contenu de l’index packwiz,
|
||||
le JAR embarqué identique au JAR construit, l’absence de classes de test dans
|
||||
le JAR et le bootstrap Prism officiel v0.0.3. Reçu :
|
||||
`build/alpha231-evidence/delivery.json`.
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| JAR Sanctuary alpha.23.1 | `906fd2e12be4233676a1a561556508eee7412183698c2cac704dc1191324f136` |
|
||||
| MRpack alpha.23.1 | `d791025721ae52a3bb7b8d385fd5930cd4fc1a2dc9a4fa255ffb72a27a2455ad` |
|
||||
|
||||
Le MRpack est **local**, pas encore publié. Le gestionnaire d’identifiants Git
|
||||
ne fournit aucun accès à `git.botsu.net` ; aucune release ni mise à jour du canal
|
||||
n’a été effectuée. Le canal public a été relu à `0.1.0-alpha.22`.
|
||||
L’instance Prism existante, ses sauvegardes et ses réglages n’ont pas été modifiés.
|
||||
Les synchronisations réelles de l’installateur et de Prism restent à effectuer
|
||||
après publication. Le ZIP d’amorçage préparé pointe vers ce canal stable ; il ne
|
||||
constitue donc pas une installation de l’alpha.23.1 à ce stade.
|
||||
|
||||
La matrice océanique ne possède pas de réouverture complète ; le monument
|
||||
océanique a son témoin de réouverture. Windows et le rendu client restent non testés.
|
||||
|
||||
## Reproduire les essais de secrets
|
||||
|
||||
Utiliser uniquement un dossier de monde de développement prévu pour la création
|
||||
correspondante. Chaque nouvelle création doit disposer d’un monde de test neuf ;
|
||||
une réouverture suit sa création avec la même graine, la même taille et les mêmes
|
||||
options. Exécuter les JVM Gradle séquentiellement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:secrets23Smoke
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySecrets23Tests=true -PsanctuaryDensityMaps=false -PsanctuaryTestSeed=42 -PsanctuaryTestDiameter=512
|
||||
./gradlew :sanctuary:runGameTest -PsanctuarySecrets23Tests=true -PsanctuaryDensityMaps=false -PsanctuaryTestSeed=42 -PsanctuaryTestDiameter=512 -PsanctuaryExpansionReload=true
|
||||
```
|
||||
|
||||
Les tailles alternatives utilisent `724` et `1024`. Les scénarios OFF ajoutent
|
||||
respectivement `-PsanctuaryExperimentalStructures=false` ou
|
||||
`-PsanctuaryGenerateStructures=false`. Ces commandes décrivent les contrôles à
|
||||
exécuter ; leur présence dans le rapport n’atteste pas leur réussite. Les rapports
|
||||
originaux du runner sont dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/secrets23-<graine>-<diamètre>/`.
|
||||
@@ -1,104 +0,0 @@
|
||||
# Validation alpha.23 — Blocodex et socle méta
|
||||
|
||||
Relevé du **10 septembre 2026**, branche `codex/blocodex-natif`.
|
||||
Cible : Minecraft **26.3-pre-2**, Fabric Loader **0.19.5**, Fabric API
|
||||
**0.160.0+26.3**, Java **25**, Sanctuary/pack **0.1.0-alpha.23**.
|
||||
Le [contrat Blocodex](blocodex.md) définit les informations et leur couverture.
|
||||
|
||||
## Résultats disponibles
|
||||
|
||||
| Vérification | Résultat observé | Preuve locale |
|
||||
| --- | --- | --- |
|
||||
| Client natif : widgets, interface et connexion au serveur intégré | **Réussie**, `BUILD SUCCESSFUL in 34s` | `build/blocodex-client-network-tests.log` |
|
||||
| Compilation et persistance quotidienne des activités | **Réussies**, `BUILD SUCCESSFUL in 9s` | `build/blocodex-meta-compile.log` |
|
||||
| Persistance de la mémoire personnelle | Smoke réussi dans la passe complète | `build/blocodex-meta-check-build.log`, ligne `Block knowledge smoke passed` |
|
||||
| GameTests natifs | **24/24 réussis**, dont 16 pour le socle méta | `build/blocodex-meta-check-build.log`, `All 24 required tests passed` |
|
||||
| Passe complète et assemblage du pack | **Réussis**, `BUILD SUCCESSFUL in 4m 18s` | `build/blocodex-meta-check-build.log` |
|
||||
|
||||
Les tests de stockage couvrent les instantanés immuables, les compteurs exacts,
|
||||
l’isolement des UUID ou des journées, les identifiants de mods absents, le JSON
|
||||
strict et le refus d’écraser silencieusement un fichier invalide. Leur succès
|
||||
est complété par les tests natifs ci-dessous.
|
||||
|
||||
Les neuf tests de connaissance vérifient poses réussies et refusées, variantes
|
||||
murales, statistiques de minage et jets volontaires, distinction des morts,
|
||||
regard occulté, inventaire/curseur, états et notifications indépendants,
|
||||
sauvegarde après déconnexion depuis un thread réseau, et palettes combinables.
|
||||
Deux tests terrain vérifient pose/remplacement/retrait, exclusion immédiate des
|
||||
comptages obsolètes, couverture et budget partagé entre lectures, sans charger
|
||||
de chunk absent. Deux tests stocks vérifient les quatre catégories, un double
|
||||
coffre ouvert sans double comptage et la conservation du butin non généré.
|
||||
Trois tests d’activité vérifient les actions natives, les quantités datées,
|
||||
la séparation des observations et des actions, la relecture après sauvegarde,
|
||||
les journées absentes et les commandes avec identifiants complets. Le test de
|
||||
fabrication appelle la complétion vanilla d’un item ; il ne joue pas une recette
|
||||
entière dans l’établi. Les huit tests généraux de génération passent aussi.
|
||||
|
||||
## Artefact local vérifié
|
||||
|
||||
Le JAR `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.jar` mesure
|
||||
**1 914 798 octets**. Sa copie dans `build/packwiz/mods/` est identique.
|
||||
SHA-256 :
|
||||
|
||||
```text
|
||||
740107901c06d1c54838a944d01431e4f134f6989e1900b294cc0c0d210b1d4f
|
||||
```
|
||||
|
||||
Le manifeste embarqué, l’entrée client, les classes de recensement et
|
||||
d’historique, le tag de palette naturelle et les paramètres des traductions
|
||||
FR/EN sont vérifiés. Les manifestes du dépôt et du pack assemblé indiquent
|
||||
`0.1.0-alpha.23`. Le contrôle pack vérifie les versions et empreintes de l’index.
|
||||
|
||||
## Protocole client exécuté
|
||||
|
||||
Le client de développement commence par une fixture explicite de **260 blocs
|
||||
en trois fragments**. Les widgets réels vérifient recherche, filtres et
|
||||
pagination, assemblage atomique, rejet des réponses périmées ou invalides,
|
||||
et conservation des grands compteurs dans la narration. Deux dimensions de
|
||||
GUI sont exercées : **854 × 480** et **427 × 240**, avec une aide défilante.
|
||||
Cette fixture ne prétend pas représenter un inventaire de joueur.
|
||||
|
||||
La seconde passe crée un **nouveau monde plat de développement**, graine 1,
|
||||
en survie, avec les commandes désactivées. Elle utilise la véritable touche
|
||||
**B**, la connexion Minecraft et les paquets du mod :
|
||||
|
||||
1. Préparer côté serveur un bloc d’or devant le joueur et trois blocs de diamant
|
||||
dans son inventaire ; laisser les observations ordinaires du serveur agir.
|
||||
2. Regarder l’or puis ouvrir B : l’or est vu sans être possédé ; le diamant est
|
||||
possédé avec un stock de 3 sans être vu ; l’émeraude reste inconnue.
|
||||
3. Fermer l’écran, retirer les diamants, puis rouvrir avec B : la possession
|
||||
passée reste vraie et le stock actuel vaut 0.
|
||||
4. Fermer le monde et vérifier le fichier de mémoire personnelle sauvegardé.
|
||||
|
||||
Les deux diagnostics signalent `passed: true` dans
|
||||
`mods/sanctuary/build/run/clientGameTest/diagnostics/` :
|
||||
|
||||
- `sanctuary-blocodex-client.json` : fixture, widgets et fragments ;
|
||||
- `sanctuary-blocodex-network-client.json` : états avant/après, touche et paquets
|
||||
réels, observation du regard et sauvegarde à la fermeture.
|
||||
|
||||
Les captures `0005_sanctuary-blocodex-network-carried-three.png` et
|
||||
`0006_sanctuary-blocodex-network-sticky-possession.png` se trouvent dans le dossier
|
||||
`screenshots/` du même client. La seconde a été inspectée : le diamant reste
|
||||
connu et possédé, avec **Inventory: 0**, dans l’interface anglaise du test.
|
||||
|
||||
## Reproduction et limites
|
||||
|
||||
Depuis la racine du dépôt, avec Java 25 et un environnement graphique :
|
||||
|
||||
```sh
|
||||
./gradlew -PsanctuaryClientTests=true :sanctuary:runClientGameTest
|
||||
./gradlew :sanctuary:materialActivitySmoke
|
||||
./gradlew check build assemblePack
|
||||
```
|
||||
|
||||
La dernière commande a terminé avec succès. Le scénario client prouve une
|
||||
connexion intégrée réelle ; il ne constitue pas une validation
|
||||
réseau à plusieurs clients sur un serveur distant.
|
||||
Les essais ne constituent pas un profil de performance d’un grand serveur.
|
||||
|
||||
Les mondes, captures, diagnostics et journaux de test restent dans les dossiers
|
||||
de développement ignorés. Le scénario réseau vérifie que sa nouvelle sauvegarde
|
||||
est créée sous `mods/sanctuary/build/run/clientGameTest/saves/`. Aucune instance
|
||||
Prism ni sauvegarde personnelle n’a été mise à jour. Aucune publication ni
|
||||
modification du canal packwiz n’a été effectuée pour ces essais.
|
||||
@@ -1,272 +0,0 @@
|
||||
# Vérifications alpha.24
|
||||
|
||||
Branche `codex/aerial-sanctuary-alpha24`, base alpha.23.1 `fbefb2e`.
|
||||
Minecraft 26.3-pre-2, Fabric Loader 0.19.5, Fabric API 0.160.0+26.3,
|
||||
Java 25 ; mod et pack `0.1.0-alpha.24`.
|
||||
|
||||
**Matrice de génération réussie le 11 septembre 2026.** Les quatorze scénarios
|
||||
passent : neuf créations, deux réouvertures, deux options OFF et l’ordre inverse
|
||||
de chargement. Les tests purs, deux admissions réelles d’expansion et la
|
||||
cascade native jusqu’à Y8 réussissent également. Les 24 GameTests généraux passent également.
|
||||
`check build assemblePack` passe en 3 min 56 s (83 tâches), avec les 24
|
||||
GameTests natifs également réussis dans ce build. JAR, MRpack, index packwiz
|
||||
et bootstrap officiel sont vérifiés. La release est publiée, le canal packwiz
|
||||
et la même instance Prism sont synchronisés ; 925 fichiers personnels et
|
||||
réglages suivis conservent leurs empreintes.
|
||||
Le [contrat de génération](generation-alpha24.md) décrit le périmètre.
|
||||
|
||||
## Environnement et preuves
|
||||
|
||||
Les essais utilisent un serveur Fabric GameTest sans interface graphique sur
|
||||
Mac. Les mondes jetables restent dans `mods/sanctuary/build/run/gameTest/` ;
|
||||
aucune sauvegarde personnelle n’est ouverte ou modifiée. Aucun essai Windows
|
||||
natif ni contrôle graphique du client avec shaders n’a été exécuté.
|
||||
|
||||
Les résultats retenus figurent dans `build/alpha24-evidence/matrix.json`,
|
||||
avec les rapports `<phase>-<graine>-<diamètre>.json` et leurs journaux `.log`.
|
||||
Un résultat n’est retenu qu’après sortie réussie du processus et
|
||||
`passed: true` dans le rapport. Ces fichiers de développement sont ignorés par
|
||||
Git. Les essais précédant la végétation sont conservés séparément dans
|
||||
`build/alpha24-evidence-before-vegetation/` et ne valident pas les formes finales.
|
||||
Ceux précédant la correction du rivage sont dans
|
||||
`build/alpha24-evidence-before-shore-fix/` ; ils attestent les contrôles déjà
|
||||
réussis, mais pas les nouveaux emplacements recherchés par le filtre corrigé.
|
||||
|
||||
Le journal `build/alpha24-shore-smoke.log` atteste les tests purs finaux.
|
||||
Les images `balloon.png`, `merchant_*.png` et `mineral_islet_*.png` sont des
|
||||
projections de contrôle calculées depuis l’export des blocs du renderer :
|
||||
ce ne sont pas des captures d’écran Minecraft. Le test FULL vérifie séparément
|
||||
que les matériaux prévus existent dans le monde natif.
|
||||
|
||||
## Résultats exécutés
|
||||
|
||||
| Essai | Résultat | Preuve |
|
||||
| --- | --- | --- |
|
||||
| Placement pur : 3 tailles × graines 0, 42, 2026 ; rotations, emprises, conflits et rivage obstrué | Réussi après correction | `alpha24-shore-smoke.log`, `Aerial24Smoke` |
|
||||
| Manifeste avant journal, relecture stricte, corruption, absence, conflits, plan vide et historique23 | Réussi | Même journal, `AerialPersistence24Smoke` |
|
||||
| Neuf créations de la matrice après correction du rivage | 9 sur 9 réussies | Tableau ci-dessous |
|
||||
| Moyen 724, graine 0, première réouverture | Réussi, 54,85 s | `reload1-0-724.json` |
|
||||
| Même monde, seconde réouverture | Réussi, 54,50 s | `reload2-0-724.json` |
|
||||
| Option expérimentale OFF, en serveur natif | Réussi, 52,93 s | `create-0-724-experimental-off.json` |
|
||||
| Structures globales OFF, en serveur natif | Réussi, 27,19 s | `create-0-724-global-off.json` |
|
||||
| Génération des chunks dans l’ordre inverse | Réussi, 81,58 s ; sept champs strictement identiques | `create-0-724-reverse.json`, `reverse-comparison.json` |
|
||||
| Cascade jusqu’à Y8 et deux admissions réelles entre les emprises | Réussi, 2 min 39 s | `flow-admission-0-724.json`, `alpha24-flow-admission.log` |
|
||||
| Comparaison du témoin historique23 avec l’alpha.23.1 | Réussie sur le code24 avant la passe végétale | `legacy23-comparison.json` |
|
||||
| GameTests généraux : Blocodex, options, population et hydrologie | 24 sur 24 réussis, JVM 2 min 2 s ; suite 54,47 s | `build/alpha24-general-gametest.log` |
|
||||
| `check build assemblePack`, contrôle des artefacts | Réussi ; 83 tâches, suite native 24/24 | `build/alpha24-check-build-pack.log`, `build/alpha24-artifacts.json` |
|
||||
|
||||
| Graine | Petit 512 | Moyen 724 | Grand 1 024 |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | Réussi, 62,64 s ; 57 chunks FULL | Réussi, 80,96 s ; 61 chunks FULL | Réussi, 128,84 s ; 48 chunks FULL |
|
||||
| 42 | Réussi, 58,25 s ; 61 chunks FULL | Réussi, 72,91 s ; 57 chunks FULL | Réussi, 92,08 s ; 58 chunks FULL |
|
||||
| 2026 | Réussi, 59,43 s ; 44 chunks FULL | Réussi, 80,77 s ; 37 chunks FULL | Réussi, 115,93 s ; 48 chunks FULL |
|
||||
|
||||
Chaque création active retrouve les cinq sites, leurs cinq coffres, deux
|
||||
marchands et une source de cascade. Les emprises réservées vont de 31 à 55
|
||||
chunks selon le monde. Les deux cas OFF retrouvent un plan vide, aucune
|
||||
réservation aérienne et aucun site aérien annoncé dans le registre.
|
||||
|
||||
L’ordre inverse, exécuté sur un nouveau Moyen, graine 0, conserve exactement
|
||||
la géométrie, le codec, les départs NBT, les blocs avant et après usage,
|
||||
les mutations du test et la végétation. `reverse-comparison.json` consigne
|
||||
l’égalité stricte de ces sept champs ; les durées et états transitoires de
|
||||
chargement ne sont pas comparés comme s’ils devaient être identiques.
|
||||
|
||||
Les durées incluent Gradle, le lancement du serveur, la préparation du monde,
|
||||
les chunks FULL demandés et les assertions. Ce ne sont ni des temps de rendu
|
||||
client, ni une mesure Windows, ni le délai nécessaire pour générer toute l’île.
|
||||
|
||||
### Correction du point de vue de rivage
|
||||
|
||||
La première matrice végétalisée avait échoué sur le Petit, graine 42 : le
|
||||
sanctuaire minéral construisait sa voûte jusqu’à Y187 au-dessus d’un point
|
||||
calculé sur le terrain brut à Y174. Le planner prépare désormais les plans
|
||||
racine avant le manifeste aérien, puis refuse leurs volumes au niveau du
|
||||
support, du point de vue et de la ligne de visée. Les assertions FULL restent
|
||||
strictes ; le cas fautif passe dans la nouvelle série. Deux tests purs
|
||||
vérifient une obstruction au rivage et au milieu du segment de vue.
|
||||
|
||||
Cette correction peut changer le candidat retenu pour un nouveau monde24.
|
||||
Un plan déjà sauvegardé reste relu tel quel : aucun recalcul implicite et
|
||||
aucune modification des anciens mondes23.
|
||||
|
||||
### Ce que le témoin Moyen vérifie
|
||||
|
||||
Les cinq sites réservent **55 chunks**, sans réserver un anneau entier. Le test
|
||||
charge **61 chunks FULL**, comprenant ces emprises, leurs points de rivage et
|
||||
le chunk qui reçoit les villageois extraits. Il vérifie notamment :
|
||||
|
||||
- Les cinq vrais départs de structures natifs, leurs pièces, leurs coordonnées,
|
||||
la sérialisation NBT et les états d’occupation conservés.
|
||||
- Cinq coffres réellement placés, leurs tables vanilla et un butin non vide,
|
||||
puis leur maintien à vide après deux redémarrages.
|
||||
- Deux villageois présents, un par navire, puis déplacés dans le chunk de test.
|
||||
Leur position reste conservée et aucun remplaçant n’apparaît dans les navires.
|
||||
- Des blocs de minerai et un élément de nacelle retirés restent absents après
|
||||
les deux réouvertures. Cette manipulation est limitée au monde de développement.
|
||||
- Des accès et intérieurs avec un sol et deux blocs libres, ainsi que les trois
|
||||
couches de couverture des coffres enterrés lors de la création.
|
||||
- Les points de rivage reposent sur le terrain FULL et ne sont pas enfouis.
|
||||
Les colonnes extérieures échantillonnées restent vides de construction.
|
||||
- Le registre passe de `PLANNED` à `GENERATED`, les recherches utilisent les
|
||||
vrais départs et le journal d’expansion reste inchangé pendant ces visites.
|
||||
|
||||
La source d’eau est présente et conservée ; l’essai séparé ci-dessous vérifie
|
||||
sa descente par les ticks de fluide jusqu’à Y8.
|
||||
|
||||
La création et les deux réouvertures contrôlent aussi la végétation réelle. Sur l’îlot avec
|
||||
cascade, **596 des 783 colonnes de surface intactes portent de l’herbe**
|
||||
(76,1 %), avec deux arbres et 27 blocs de hauteur rocheuse. Sur l’îlot sec,
|
||||
ce sont **1 506 sur 1 788** (84,2 %), quatre arbres et 21 blocs de roche.
|
||||
Chaque enveloppe ajoute huit blocs pour les couronnes. Les troncs ont un appui
|
||||
en terre, les couronnes restent dans l’emprise et les feuilles utilisent les
|
||||
propriétés natives de distance, sans persistance forcée.
|
||||
`persistence-comparison.json` confirme également l’égalité de la géométrie,
|
||||
du codec, des départs natifs, des blocs après usage, des mutations et des
|
||||
mesures de végétation dans les deux relectures.
|
||||
|
||||
### Admission réelle et écoulement natif
|
||||
|
||||
`flow-admission-0-724.json` et `build/alpha24-flow-admission.log` attestent le
|
||||
test dans un autre monde jetable Moyen, graine 0. Il appelle le véritable
|
||||
`ExpansionRuntime.Session.create` avant que les satellites soient visités.
|
||||
|
||||
| Expansion de test, diamètre 128 | Premier centre envisagé X Z | Centre réellement réservé X Z | Appel de réservation | Résultat |
|
||||
| --- | --- | --- | ---: | --- |
|
||||
| `aerial_collision_0_southeast` | `496 336`, chevauche un satellite | `368 496` | 114 ms | `READY`, un départ vanilla valide |
|
||||
| `aerial_interval_0_northeast` | `400 -432`, passage libre | `400 -432` | 54 ms | `READY`, un départ vanilla valide |
|
||||
|
||||
La première demande filtre le candidat en conflit, puis poursuit les
|
||||
vérifications d’adaptation avant sa réservation définitive. La seconde
|
||||
montre que la bande entre les satellites reste utilisable. Les deux
|
||||
réservations sont idempotentes, respectent leurs emprises et atteignent
|
||||
`READY` avec une structure native adaptée. Ces durées d’appels ne représentent
|
||||
pas la préparation complète, comprise dans le test de 2 min 39 s.
|
||||
|
||||
Après chargement et activation des ticks des chunks concernés, l’unique
|
||||
source prévue produit de l’eau sous l’îlot à `399 194 312`, puis à
|
||||
`399 8 312`, après **1 180 ticks d’attente contrôlée**. À Y8, le fluide
|
||||
est de l’eau courante, pas une source artificiellement posée. Aucun chemin
|
||||
vertical de blocs sources n’est fabriqué par le renderer. Cet essai valide
|
||||
ce trajet réel ; il ne garantit pas la forme de l’écoulement après des
|
||||
constructions ou excavations effectuées par les joueurs.
|
||||
|
||||
### Chargement des habitants dans le test Grand
|
||||
|
||||
Le premier essai Grand, graine 0, a rencontré un délai de visibilité des
|
||||
entités : les marchands existaient sur disque mais n’étaient pas encore
|
||||
exposés au test. Le protocole de lecture charge désormais les navires entiers,
|
||||
vérifie la disponibilité native des entités et attend au plus 600 ticks.
|
||||
L’exigence reste de deux marchands distincts, sans création ou remplacement
|
||||
par le test. Les trois Grands passent ensuite : le témoin graine0 attend
|
||||
250 ticks et celui de graine2026, 323 ticks, avant de voir les deux habitants.
|
||||
Cela confirme que l’ancienne limite de 200 ticks du fixture était trop courte.
|
||||
Le premier essai interrompu n’est pas compté dans les réussites de la matrice.
|
||||
|
||||
## Repères de visite — Moyen 724, graine 0
|
||||
|
||||
Créer un **nouveau monde Sanctuary de génération24**, taille Moyen, graine `0`,
|
||||
avec **Générer les structures** et **Structures expérimentales** activés.
|
||||
Les coordonnées proviennent de `create-0-724.json`, champ `look_views`.
|
||||
Le nouveau rapport après correction du rivage confirme ces mêmes coordonnées.
|
||||
Les repères de rivage désignent un point de vue deux blocs au-dessus du témoin
|
||||
enregistré ; ils ne constituent pas des téléportations de survie exécutées.
|
||||
|
||||
| Lieu | Entrée X Y Z | Point de vue au rivage X Y Z | Distance horizontale entrée–rivage |
|
||||
| --- | --- | --- | ---: |
|
||||
| Montgolfière | `-492 288 110` | `-251 219 55` | 247,2 blocs |
|
||||
| Navire de vivres | `-35 186 -503` | `-19 176 -282` | 221,6 blocs |
|
||||
| Navire de matériel | `463 186 -204` | `229 159 -102` | 255,3 blocs |
|
||||
| Îlot avec cascade | `399 237 333` | `195 161 163` | 265,5 blocs |
|
||||
| Îlot sec | `-277 256 456` | `-125 227 207` | 291,7 blocs |
|
||||
|
||||
Pour un repérage de développement, `/locate structure sanctuary:aerial_balloon`,
|
||||
`sanctuary:aerial_merchant_ship` et `sanctuary:aerial_mineral_islet` utilisent
|
||||
les types natifs. `/sanctuary census structures aerial` distingue les cinq
|
||||
sites. Aucun pont ou véhicule n’est ajouté pour les atteindre. La limite de
|
||||
512 blocs depuis un rivage ne force pas les réglages de rendu du client ou
|
||||
du serveur, ni la visibilité à travers les feuillages.
|
||||
|
||||
## Historique23
|
||||
|
||||
Le rapport `legacy23-0-724.json`, produit sur un monde de génération23 avec le
|
||||
code24 avant l’ajout de végétation aérienne, a été comparé à
|
||||
`build/alpha231-evidence/final-medium.json`. Tous les champs sont identiques,
|
||||
à l’exception attendue de `process_id` : codec, plans, départs natifs,
|
||||
géométrie, rails et traversées contrôlés dans **85 chunks FULL**.
|
||||
`legacy23-comparison.json` consigne cette égalité.
|
||||
|
||||
Cette preuve porte sur ce témoin précis, pas sur toutes les graines ni toutes
|
||||
les dimensions. Les tests de persistance conservent aussi les octets du journal23
|
||||
et refusent une conversion implicite vers24. Aucun monde existant ne reçoit
|
||||
automatiquement les satellites24 ; le nouveau manifeste appartient uniquement
|
||||
aux nouveaux mondes24.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Depuis la racine du dépôt, exécuter les commandes **séquentiellement**.
|
||||
`runGameTest` recrée son monde de développement par défaut. Pour les phases
|
||||
de réouverture, garder le même dossier et ajouter impérativement
|
||||
`sanctuaryExpansionReload=true`.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:aerial24Smoke :sanctuary:aerialPersistence24Smoke
|
||||
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryAerial24Tests=true \
|
||||
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryAerial24Phase=create -PsanctuaryDensityMaps=false
|
||||
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryAerial24Tests=true \
|
||||
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryAerial24Phase=reload1 -PsanctuaryExpansionReload=true \
|
||||
-PsanctuaryDensityMaps=false
|
||||
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryAerial24Tests=true \
|
||||
-PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=0 \
|
||||
-PsanctuaryAerial24Phase=reload2 -PsanctuaryExpansionReload=true \
|
||||
-PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
La matrice de créations reprend les diamètres `512`, `724`, `1024` et les
|
||||
graines `0`, `42`, `2026`. Les variantes du Moyen, graine0, ajoutent séparément
|
||||
`-PsanctuaryExperimentalStructures=false`,
|
||||
`-PsanctuaryGenerateStructures=false` ou
|
||||
`-PsanctuaryAerial24ReverseChunks=true` à la commande de création.
|
||||
L’essai d’admission et d’eau utilise un nouveau monde :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest \
|
||||
-PsanctuaryAerial24FlowAdmission=true \
|
||||
-PsanctuaryTestGeneration=24 -PsanctuaryTestDiameter=724 \
|
||||
-PsanctuaryTestSeed=0 -PsanctuaryDensityMaps=false
|
||||
```
|
||||
|
||||
Cette dernière commande reproduit l’essai d’admission et d’écoulement réussi.
|
||||
Les assertions sont dans
|
||||
[Aerial24WorldGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24WorldGameTests.java)
|
||||
et [Aerial24FlowAdmissionGameTests](../mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Aerial24FlowAdmissionGameTests.java).
|
||||
|
||||
## Limites et distribution
|
||||
|
||||
Les essais ne valident pas encore le ressenti en vol, la lisibilité dans le
|
||||
client, l’équilibrage économique des filons, les performances multijoueurs ou
|
||||
les incidents Windows rapportés sur cette préversion Minecraft. Le test pur
|
||||
des neuf plans emploie un terrain synthétique ; seule la matrice FULL valide
|
||||
leur adaptation au relief réel des graines retenues.
|
||||
|
||||
La [release alpha.24](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.24)
|
||||
est publiée depuis `27d7cba`. Le canal packwiz (`bd9b4c4`) et la même instance
|
||||
Prism sont synchronisés, avec deux passages isolés puis deux sur l’instance.
|
||||
Le second passage conserve les fichiers gérés identiques. Un seul JAR Sanctuary
|
||||
alpha.24 est installé ; les 925 fichiers personnels et réglages suivis sont
|
||||
inchangés. Aucune sauvegarde personnelle n’a été ouverte ou régénérée.
|
||||
Voir [la distribution et les empreintes](packwiz.md).
|
||||
|
||||
Preuves locales : `build/alpha24-artifacts.json`,
|
||||
`build/alpha24-isolated-validation.json`, `build/alpha24-prism-validation.json`
|
||||
et `build/alpha24-distribution-status.json`. Le build final dure 3 min 56 s,
|
||||
avec 83 tâches et les 24 GameTests natifs réussis en 52,92 s. Ces durées ne
|
||||
constituent pas une mesure des performances du client.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Alpha.27 — vérifications de reprise
|
||||
|
||||
Base de travail : `e7d1f29` (alpha.24), branche
|
||||
`codex/reprise-sanctuary-alpha27`. Mod et pack : `0.1.0-alpha.27`, Minecraft
|
||||
`26.3-pre-2`. Nouveau monde Sanctuary, Moyen 724 par défaut.
|
||||
|
||||
Les fichiers de densité et les ressources de relief de la 24 sont conservés.
|
||||
Les deux changements localisés concernent les biomes de marais et les lieux
|
||||
aériens. Voir le [périmètre exact](generation-alpha27.md).
|
||||
|
||||
## Résultat local du 12 septembre 2026
|
||||
|
||||
`./gradlew check build` réussit en **3 min 28 s**. Le serveur de développement
|
||||
termine ses **19 tests obligatoires**, dont les témoins marais et aériens, sans
|
||||
échec. Le journal faisant foi est `build/alpha27-check-build-final.log` ; les
|
||||
premiers journaux de préparation ne décrivent pas ce résultat final.
|
||||
|
||||
Commande exacte :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerialPreview -PsanctuaryAerial24Tests=true
|
||||
./gradlew assemblePack -x :sanctuary:check
|
||||
```
|
||||
|
||||
La première option désactive les images de densité facultatives. Le contrôle
|
||||
`check` vient de réussir et n’est pas répété pendant l’assemblage du pack.
|
||||
Les 56 fichiers de densité, relief et hydrologie recensés sont identiques à
|
||||
la base `e7d1f29` (`build/alpha27-evidence/terrain-base.json`).
|
||||
|
||||
Les deux reçus natifs ci-dessous proviennent du même processus `48406`, dans
|
||||
un **nouveau monde Moyen, diamètre 724, graine 0**, avec les structures et
|
||||
l'option expérimentale activées. Ils ont été écrits à 00:32:38 et 00:32:47,
|
||||
pendant le dernier passage réussi. Le générateur reste `sanctuary:island_v24`
|
||||
dans cette reprise ; la version du mod est bien alpha.27.
|
||||
|
||||
## Marais souterrain réel
|
||||
|
||||
`Swamp27WorldGameTests` réutilise le plan hydrologique central déjà en cache,
|
||||
puis inspecte au maximum huit chunks FULL demandés explicitement. Aucune
|
||||
région supplémentaire n'est planifiée pour sélectionner les eaux témoins ;
|
||||
aucun bloc ni point d'eau n'est posé par le test.
|
||||
|
||||
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/swamp27/native.json`
|
||||
confirme :
|
||||
|
||||
- **1 444 sols de cavités sous voûte**, dont 1 315 couverts d'herbe, mousse ou boue ;
|
||||
- **355 blocs d'eau** dans le biome de marais ;
|
||||
- **494 plantes** : 107 buissons à lucioles, 292 herbes courtes et 95 lianes ;
|
||||
- des eaux souterraines existantes à Y118, 120, 129 et 130 ; 289 colonnes d'eau
|
||||
recoupent le champ du marais dans la région inspectée.
|
||||
|
||||
Repère pour visiter ce témoin dans un nouveau monde de mêmes paramètres :
|
||||
|
||||
```text
|
||||
/tp @s -6.5 130 -92.5
|
||||
```
|
||||
|
||||
`diagnostics/swamp27/blocks.json` exporte une fenêtre des **blocs FULL réels**
|
||||
et une coupe destinée à l'aperçu. Le monde reste intact. Cette projection ne
|
||||
reproduit pas l'éclairage, les shaders ou le rendu exact du client Minecraft.
|
||||
|
||||
## Quatre lieux aériens
|
||||
|
||||
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/aerial24/create.json`
|
||||
porte `passed: true` et confirme **deux bateaux, deux îlots, deux marchands
|
||||
réels, quatre coffres et une source de cascade**. Les deux marchands sont
|
||||
observés après chargement effectif des entités. Les 56 chunks du témoin
|
||||
aérien incluent les structures et leurs points de rivage.
|
||||
|
||||
Les coques lisent le vrai template Minecraft `shipwreck/rightsideup_full`,
|
||||
palette 0 : 643 des 659 blocs physiques d’origine sont conservés lors de la
|
||||
restauration, 16 remplacés et 76 ajoutés avant le mobilier marchand. Les
|
||||
escaliers, dalles et la cabine native restent présents. Les aperçus de blocs
|
||||
simplifiés ont été inspectés ; ils ne sont pas des captures du client.
|
||||
|
||||
Le test contrôle les blocs posés, les accès, les coffres enterrés des îlots,
|
||||
les starts natifs et leur sérialisation. Il ouvre les quatre coffres,
|
||||
déplace les deux marchands et retire quelques blocs dans le monde jetable,
|
||||
puis sauvegarde. Les quatre sites restent enregistrés comme générés ; le
|
||||
journal d'expansion est inchangé.
|
||||
|
||||
Les tests purs couvrent également **neuf combinaisons** de graines 0/42/2026
|
||||
et diamètres 512/724/1024 : déterminisme des plans, orientations, limites et
|
||||
réservations. Ce sont des calculs de plans et de géométrie, **pas neuf mondes
|
||||
natifs intégralement générés**.
|
||||
|
||||
## Limites
|
||||
|
||||
Cette passe ne valide pas une nouvelle matrice de rechargements, toutes les
|
||||
graines ni toute l'île. Elle ne garantit pas des grenouilles ou des chênes
|
||||
dans chaque cavité. Le premier témoin sec a servi à corriger la sélection
|
||||
du test ; les chiffres ci-dessus proviennent du témoin humide final.
|
||||
|
||||
Aucun essai Windows natif, mesure de FPS ou validation visuelle par le joueur
|
||||
n'est revendiqué. La durée du build comprend les vérifications et ne mesure
|
||||
pas le temps de chargement d'une partie. La publication des artefacts et la
|
||||
synchronisation Prism font l'objet de reçus de livraison distincts.
|
||||
|
||||
## Livraison
|
||||
|
||||
`assemblePack` réussit. JAR, MRpack, dépendances et ZIP d’amorçage vérifiés,
|
||||
puis téléchargement anonyme des mêmes artefacts. Deux installations isolées
|
||||
et deux synchronisations de l’instance Prism existante réussissent ; les
|
||||
925 fichiers personnels et réglages suivis sont inchangés. Voir
|
||||
[la livraison](packwiz.md#alpha27-publiée--12-septembre-2026).
|
||||
@@ -1,110 +0,0 @@
|
||||
# Alpha.28 — vérifications
|
||||
|
||||
## Protocole
|
||||
|
||||
Tests sur macOS avec Java 25 et Minecraft 26.3-pre-2. Aucun essai Windows
|
||||
natif ni jugement de rendu client n’est revendiqué. Le créateur a validé
|
||||
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
|
||||
nouvelle passe, sans représenter l’eau, la végétation ou les structures.
|
||||
|
||||
Commande de livraison :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true
|
||||
```
|
||||
|
||||
`sanctuaryDensityMaps=false` coupe la production des anciennes images,
|
||||
pas les assertions. Le monde GameTest est recréé dans le dossier de développement
|
||||
ignoré. Les sauvegardes personnelles ne sont pas utilisées par les tests.
|
||||
|
||||
Les smokes de placement aérien examinent 512 / 724 / 1 024, graines 0 / 42 /
|
||||
2026. Le témoin natif chargé en chunks FULL est Moyen 724, graine 0 ; ceci
|
||||
ne prétend pas être neuf îles entièrement explorées. Les comparaisons de
|
||||
terrain analysent deux coupes espacées de 2 blocs et une grille de hauteurs
|
||||
espacée de 32 blocs. Une expansion naturelle de 512 est contrôlée par son
|
||||
vrai champ de densité sans être réservée ni ouverte dans le serveur.
|
||||
|
||||
## Résultats
|
||||
|
||||
`check build` passe en **4 min 15 s**, dont **21/21 GameTests natifs**
|
||||
en 57,06 s. `assemblePack` passe ensuite sans recompilation des binaires vérifiés.
|
||||
Les smokes couvrent aussi le Blocodex, les formats de journal, les priorités
|
||||
et les réservations d’expansion, ainsi que les anciennes géométries pures.
|
||||
|
||||
Le terrain de référence conserve **6 098 des 6 860** anciens échantillons
|
||||
rocheux intérieurs (88,9 %), avec **762** désormais creusés. Cette comparaison
|
||||
inclut les raccords du nouveau cisaillement, pas seulement les grottes.
|
||||
L’expansion de 512 conserve 1 299 échantillons contre 136 creusés.
|
||||
|
||||
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 s’abaissent.
|
||||
La hausse maximale relevée est de 32 blocs et le sommet échantillonné atteint
|
||||
Y280. La baisse maximale de sommet, 132 blocs en (0,160), correspond à un
|
||||
lobe supérieur qui quitte cette colonne : le sol observé passe de Y248 à
|
||||
Y116. Elle ne désigne pas un déplacement vertical de 132 blocs ; le déplacement
|
||||
3D est borné à ±40 en Y. Des surplombs et étranglements subsistent et restent
|
||||
à apprécier visuellement dans le jeu.
|
||||
|
||||
Les **9 chunks FULL** inspectés contiennent les huit minerais Overworld :
|
||||
|
||||
| Minerai | Blocs observés, variantes deepslate comprises |
|
||||
|---|---:|
|
||||
| Charbon | 1408 |
|
||||
| Fer | 325 |
|
||||
| Cuivre | 302 |
|
||||
| Or | 87 |
|
||||
| Lapis | 53 |
|
||||
| Redstone | 77 |
|
||||
| Diamant | 16 |
|
||||
| Émeraude | 4 |
|
||||
|
||||
Il s’agit d’un relevé après génération, pas de quotas imposés à chaque chunk.
|
||||
|
||||
La relecture finale des fichiers de région confirme **trois donjons vanilla**
|
||||
avec spawners zombies et coffres `minecraft:chests/simple_dungeon` :
|
||||
`-101 170 100`, `-92 175 -92` et `-85 178 11`. Une vraie mine est présente
|
||||
vers `152 176 -120`, avec **143 pièces sauvegardées**. Dans les 14 chunks FULL
|
||||
du contrôle des structures : 91 planches, 88 barrières, 13 rails, 29 toiles
|
||||
et un spawner ont été observés dans les pièces chargées. Ce relevé sur les
|
||||
fichiers MCA après fermeture du serveur ne génère aucun chunk supplémentaire.
|
||||
|
||||
Les quatre lieux aériens ont de vrais départs et pièces sauvegardées,
|
||||
quatre coffres et deux marchands adultes au total, puis passent les contrôles
|
||||
après ouverture, extraction et modification dans le protocole natif. Il y a
|
||||
exactement une source de cascade et deux îlots survolant le terrain. Aucun
|
||||
nouveau site ni habitant n’est recréé pour remplacer ce qui a été utilisé.
|
||||
|
||||
Preuves locales ignorées : `build/alpha28-check-build-final.log`,
|
||||
`build/alpha28-preview/geology-final.json`,
|
||||
`build/alpha28-preview/geology-final-comparison.png`,
|
||||
`build/alpha28-evidence/final/` et reçus de distribution `build/alpha28-*`.
|
||||
|
||||
|
||||
## Limites et essai du créateur
|
||||
|
||||
- La hutte de sorcière est conditionnelle aux berges 3D, à l’espace et aux quatre
|
||||
appuis natifs. Le premier petit échantillon n’en contient aucune : son audit
|
||||
de placement ne constitue pas une preuve de présence en jeu.
|
||||
- Les quatre départs et les deux UUID de marchands persistants sont relus
|
||||
dans les données sauvegardées. Aucun second lancement JVM avec ce monde
|
||||
alpha.28 n’est revendiqué.
|
||||
- La source de la cascade est contrôlée ; son écoulement complet jusqu’au vide
|
||||
n’est pas mesuré par ce protocole. La propagation reste celle de Minecraft.
|
||||
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
|
||||
l’ancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
|
||||
que le nouveau relief serait identique à celui de la 27.
|
||||
- Il ne s’agit pas d’un benchmark client ni d’une simulation physique tectonique.
|
||||
Les performances pourront être étudiées après ce retour visuel.
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0** pour
|
||||
retrouver le témoin. Garder ses autres sauvegardes ; aucune migration ni
|
||||
régénération n’est réalisée. Valider en priorité les masses soulevées et
|
||||
abaissées, les cavités à explorer, les îlots au-dessus du terrain et les
|
||||
navires proches des berges.
|
||||
|
||||
## Distribution
|
||||
|
||||
Le JAR, le MRpack et le ZIP d’amorçage ont les mêmes empreintes sur disque
|
||||
et après téléchargement anonyme de la release. Les deux synchronisations
|
||||
isolées sont identiques ; les deux synchronisations de l’instance Prism
|
||||
existante conservent les empreintes des 925 fichiers personnels et réglages
|
||||
suivis. Un seul JAR alpha.28 est actif. Voir [le reçu de livraison](packwiz.md).
|
||||
@@ -1,109 +0,0 @@
|
||||
# Alpha.29 — vérifications
|
||||
|
||||
**Point de reprise local, non publié.** Le chantier se poursuit directement
|
||||
en alpha.30 à la demande du créateur. Le contrôle supplémentaire Petit512,
|
||||
graine2026, a refusé la traversée et révélé un témoin côtier aérien sans appui
|
||||
après génération. Ces limites empêchent de considérer la29 comme livrée.
|
||||
Les artefacts préparatoires et reçus locaux restent des preuves intermédiaires.
|
||||
|
||||
État des preuves au 12 septembre 2026. Les essais natifs ci-dessous ont été
|
||||
exécutés sur **Mac, en serveur de développement sans rendu client**. Le retour
|
||||
Windows du créateur concerne l’alpha.28 ; aucun essai Windows de la 29 n’est
|
||||
revendiqué. Créer un **nouveau monde** pour tester cette génération.
|
||||
|
||||
## Traversées réellement générées et parcourues
|
||||
|
||||
| Monde | Départs / chunks FULL | Distance entre accès | Parcours avec collisions natives |
|
||||
|---|---:|---:|---:|
|
||||
| Grand 1 024, graine **-7228211907433324401** | 23 / 52 | 340 blocs | 652 pas, 74 points accessibles |
|
||||
| Moyen 724, graine **0** | 17 / 27 | 238 blocs | 390 pas, 56 points accessibles |
|
||||
|
||||
Chaque témoin vérifie les blocs posés, les escaliers, les approches naturelles,
|
||||
les raccords entre modules, les départs sauvegardables et le locator d’entrée.
|
||||
Les **cinq cellules du côté fermé de la jonction en T** sont contrôlées après
|
||||
la correction du renderer. Ces recherches n’ouvrent aucune expansion.
|
||||
|
||||
Preuves : `build/alpha29-evidence/user-locate-before-crossing/transit29-native.json`
|
||||
(PID 55232) et `build/alpha29-evidence/medium-0/` (reçus du PID 56574).
|
||||
La correction ultérieure de l’admission des mines ne modifie pas le transit.
|
||||
|
||||
Sur la même graine Grand, la référence alpha.28 produisait un plan vide après
|
||||
419 alignements et 13 000 colonnes. Cette reproduction est conservée dans
|
||||
`build/alpha29-evidence/baseline-user/` ; le succès de ses anciens GameTests
|
||||
ne démontrait pas la présence d’une traversée.
|
||||
|
||||
## Relief : comparaison exacte avec la 28
|
||||
|
||||
| Monde | Densités inférieures identiques bit à bit | Échantillons supérieurs ayant changé de signe |
|
||||
|---|---:|---:|
|
||||
| Grand, graine signalée | 10 140 | 303 / 71 656, soit **0,423 %** |
|
||||
| Moyen, graine 0 | 7 935 | 166 / 56 074, soit **0,296 %** |
|
||||
|
||||
Les densités inférieures sont échantillonnées entre Y32 et Y214. Les deux
|
||||
coupes à Z0 et Z96, au pas d’un bloc dans la bande Y205–305, ont été comparées
|
||||
pour la graine signalée : petites corniches, quelques arêtes plus dentelées,
|
||||
grandes silhouettes de la 28 conservées. Cela ne remplace pas un essai visuel
|
||||
dans le client. Le déplacement supplémentaire des coordonnées est borné à
|
||||
2,5 blocs ; cette borne ne porte pas sur le changement de sommet lorsqu’une
|
||||
petite lèvre disparaît.
|
||||
|
||||
Reçus `alpha29-terraces-*.json` des dossiers de preuve ; aperçu local :
|
||||
`build/alpha29-preview/terraces-1024--7228211907433324401.png`.
|
||||
|
||||
## Mine vanilla et satellites
|
||||
|
||||
**Moyen, graine 0 :** une véritable mine vanilla de **143 pièces acceptées**
|
||||
est trouvée parmi les candidats normaux. L’inspection est partielle :
|
||||
**16 chunks FULL** sont demandés par le test. Elle observe notamment
|
||||
244 planches de chêne, 200 barrières, 102 rails et 50 toiles dans les pièces
|
||||
chargées. Une salle `monster_room` avec spawner de zombies est également
|
||||
observée. Le test ne prétend pas avoir exploré les 143 pièces, ni avoir
|
||||
déclenché tous les spawners ou contrôlé tous les butins. La hutte de sorcière
|
||||
est absente de cet échantillon ; ce n’est pas une absence prouvée sur l’île.
|
||||
|
||||
Une lecture séparée du monde sauvegardé confirme deux spawners vanilla
|
||||
`cave_spider` aux positions **156 171 -138** et **140 175 -97**, dans les
|
||||
corridors correspondants (`sc=1`, `hps=1`). Reçu :
|
||||
`build/alpha29-evidence/medium-0/mineshaft-spawners-saved.json`.
|
||||
|
||||
Sur ce même monde, **quatre satellites**, **deux villageois**, **quatre coffres**
|
||||
et **une source de cascade** passent la création puis un véritable redémarrage.
|
||||
Les 47 chunks FULL contrôlés conservent le pillage, les villageois déplacés
|
||||
et les blocs détruits, sans duplication ni reconstruction des témoins.
|
||||
Le journal d’expansion reste inchangé. Création PID 56060, rechargement
|
||||
PID 56574 ; les **23 GameTests natifs** de ce dernier processus passent.
|
||||
Preuves regroupées dans `build/alpha29-evidence/medium-0/`.
|
||||
|
||||
## Tests ciblés et portée
|
||||
|
||||
Les smokes terrain, appuis de mines et transit couvrent les bornes, les
|
||||
orientations et les refus protégés. Le test pur de traversée couvre
|
||||
0 / 42 / 2026 × 512 / 724 / 1 024 **sur un terrain synthétique portant** :
|
||||
ce n’est pas une matrice de neuf mondes Minecraft réellement générés.
|
||||
Les contrôles natifs ci-dessus portent sur les témoins explicitement cités.
|
||||
|
||||
`./gradlew check build` passe : **86 tâches**, dont les **23 GameTests natifs**,
|
||||
en 2 min 58 s pour cette exécution. Journal :
|
||||
`build/alpha29-evidence/medium-0/gradle.log`. Ce temps n’est pas un benchmark de génération.
|
||||
|
||||
`./gradlew assemblePack` passe également. JAR, MRpack autonome, index packwiz
|
||||
et ZIP d’amorçage sont vérifiés localement : versions, dépendances, CRC et
|
||||
empreintes concordent. Reçus `build/alpha29-artifacts.json` et
|
||||
`build/alpha29-delivery-local.json`.
|
||||
|
||||
Restent en attente de leurs reçus :
|
||||
- Contrôles natifs de **planification seule** Grand/graine42 et Petit/graine2026 ;
|
||||
même réussis, ils ne prouveront pas un parcours sur des chunks FULL.
|
||||
- Publication, canal packwiz et synchronisation Prism.
|
||||
|
||||
Pas de preuve d’exécution avec structures désactivées propre à l’alpha.29,
|
||||
ni de nouvelle validation visuelle client ou Windows à ce stade.
|
||||
|
||||
## Repères pour un essai manuel
|
||||
|
||||
Avec les structures activées, sur un nouveau Grand de graine
|
||||
`-7228211907433324401`, les entrées parcourues sont **128 252 -127** et
|
||||
**128 257 213**. Sur Moyen/graine0 : **-90 235 -127** et **-90 235 111**.
|
||||
`/locate structure sanctuary:industrial_transit` recherche une entrée du réseau.
|
||||
Le repère **152 175 -120** désigne la mine de Moyen/graine0 : l’utiliser en
|
||||
spectateur pour l’inspection, sans supposer une arrivée sûre en survie.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Alpha.30.1 — vérifications
|
||||
|
||||
Essais natifs sur **Mac**, le 12 septembre 2026, en serveur de développement
|
||||
sans rendu client. Aucun essai Windows natif de ce correctif n’est revendiqué.
|
||||
|
||||
## Moyen 724, graine 0
|
||||
|
||||
Processus **64315** : **24/24 GameTests passent**, en 2 min 30 s pour la commande.
|
||||
Reçus dans `build/alpha301-evidence/medium-0/`.
|
||||
|
||||
Deux brèches sont rattachées à la véritable **ancienne rivière courbe**, et non
|
||||
à un nouveau tracé. Sources : **13 247 79** et **-105 247 -69** ; sorties :
|
||||
**13 246 82** et **-105 246 -65**. Elles traversent respectivement trois et
|
||||
quatre blocs de berge. Le test sous ticks réels observe l’écoulement en dessous.
|
||||
Une lecture séparée de la sauvegarde confirme des colonnes continues d’eau aux
|
||||
sorties, de **Y245 à Y225** et de **Y246 à Y226** à ce stade de simulation.
|
||||
Aucune source n’a été ajoutée par la brèche. Cela ne prétend pas avoir attendu
|
||||
leur propagation jusqu’au fond du monde. Preuve : `saved-water-columns.json`.
|
||||
|
||||
La traversée est réellement générée : **19 modules, 28 chunks FULL, 272 blocs
|
||||
entre les accès, 478 pas contrôlés et 62 points accessibles**. Entrées :
|
||||
**90 248 -144** et **90 225 128**. Le sanctuaire possède son départ natif et
|
||||
un sol central libre au-dessus. Les quatre satellites sont contrôlés.
|
||||
|
||||
Une mine vanilla de **143 pièces** est retenue ; le test inspecte un échantillon
|
||||
de 16 chunks FULL. Il trouve notamment 244 planches de chêne, 200 barrières,
|
||||
102 rails, 50 toiles et un spawner dans les pièces chargées. Point de visite :
|
||||
**152 175 -120**. Ce n’est pas une visite de la totalité de la mine.
|
||||
|
||||
Comparaison avec le relief de la 28 : **6 348 densités inférieures inchangées**,
|
||||
et seulement **466 / 74 060** signes modifiés dans les échantillons hauts
|
||||
(**0,63 %**). Ce changement est localisé dans la bande de plateau ; le reste
|
||||
utilise exactement le poids global de déformation précédent.
|
||||
|
||||
## Grand 1 024, graine -7228211907433324401
|
||||
|
||||
Le premier passage natif (processus **64637**) vérifie la traversée complète :
|
||||
**23 modules, 52 chunks FULL, 374 blocs entre les accès, 788 pas contrôlés et
|
||||
71 points accessibles**. Entrées : **192 238 -195** et **192 277 179**.
|
||||
Le sanctuaire minéral et les quatre satellites sont également retenus.
|
||||
|
||||
La comparaison au relief 28 conserve **8 112 densités inférieures** ;
|
||||
**940 / 94 640** signes hauts diffèrent (**0,99 %**). La recherche native
|
||||
échantillonnée observe des portails en ruine et un camp, mais pas de mine dans
|
||||
ce témoin limité. La preuve de mine de cette livraison est celle du Moyen 0.
|
||||
|
||||
La région centrale de cette graine contient des lacs et bassins, sans ancienne
|
||||
rivière admise. Le premier passage a donc révélé une assertion de test trop
|
||||
stricte : elle exigeait une rivière sur chaque témoin, contrairement au caractère
|
||||
conditionnel de l’hydrologie d’origine. Le test est corrigé sans forcer une
|
||||
rivière dans le générateur. Le témoin spécifique de berge de rivière peut être
|
||||
exigé avec `-PsanctuaryRequireCurvedRiverBank=true` sur une graine adaptée.
|
||||
|
||||
Le redémarrage (processus **65123**) passe les **24/24 GameTests** :
|
||||
`check build` réussit en **4 min 19 s**. Le contrôle d’eau inspecte 8 chunks
|
||||
FULL et trouve 7 positions d’eau sous les deux brèches testées. Sorties :
|
||||
**-14 236 41** et **-50 237 -80**. La traversée et le sol du sanctuaire sont
|
||||
revérifiés. Les satellites sont rechargés après pillage, extraction d’un
|
||||
villageois et démolition partielle : les témoins ne sont pas reconstruits et
|
||||
le villageois n’est pas dupliqué. Reçus :
|
||||
`build/alpha301-evidence/large-user/reload/`.
|
||||
|
||||
Commande de vérification :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryTestDiameter=1024 \
|
||||
-PsanctuaryTestSeed=-7228211907433324401 -PsanctuaryDensityMaps=false \
|
||||
-PsanctuaryAerial24Tests=true -PsanctuaryAerial24Phase=reload1 \
|
||||
-PsanctuaryExpansionReload=true -PsanctuaryTransit29Tests=true
|
||||
```
|
||||
|
||||
## Tests ciblés
|
||||
|
||||
`river30Smoke` passe : ouverture à travers une berge sèche existante, conservation
|
||||
exacte des points du tracé et des cellules d’eau, absence de nouvelle source,
|
||||
déterminisme et conservation d’une berge intérieure éloignée du vide.
|
||||
Une brèche de cinq blocs possède une légère descente pour l’écoulement vanilla.
|
||||
Les trois tailles conservent le poids global exact sur plus de 85 % de leur
|
||||
emprise circulaire ; le cœur de plateau couvre moins de 8 %.
|
||||
|
||||
Les brèches restent conditionnelles à la proximité d’un espace ouvert. Ces tests
|
||||
ne justifient pas d’annoncer une cascade pour tous les bassins ou toutes les graines.
|
||||
La taille Petit est couverte par les tests de champ et les tests synthétiques ;
|
||||
elle n’a pas fait l’objet d’un nouveau passage natif complet pour ce correctif.
|
||||
`assemblePack` passe après le `check build` réussi ; la tâche `:sanctuary:check`
|
||||
est exclue de cette seconde commande pour ne pas répéter la suite inchangée.
|
||||
Le MRpack autonome, le JAR intégré et le ZIP d’amorçage Prism sont vérifiés
|
||||
localement : versions, index, empreintes et contenu concordent. Reçus :
|
||||
`build/alpha301-artifacts.json` et `build/alpha301-delivery-local.json`.
|
||||
La publication et la synchronisation sont consignées dans [Distribution](packwiz.md). Créer un nouveau monde pour l’essai ; aucune sauvegarde personnelle
|
||||
n’est régénérée.
|
||||
|
||||
## Distribution terminée
|
||||
|
||||
Release et canal publiés ; téléchargement public des trois artefacts conforme
|
||||
aux empreintes locales. Deux synchronisations isolées puis deux synchronisations
|
||||
de la même instance Prism **Sanctuary Beta** réussissent. Les **925 fichiers
|
||||
personnels et réglages suivis** conservent leurs empreintes. Voir les versions,
|
||||
commits et reçus dans [Distribution](packwiz.md#alpha301-publiée--12-septembre-2026).
|
||||
@@ -1,100 +0,0 @@
|
||||
# Alpha.30.2 — vérifications et limites
|
||||
|
||||
Essais sur **Mac**, le 12 septembre 2026, dans le serveur de développement.
|
||||
Aucun essai Windows natif de ce correctif n’est revendiqué. Les comparaisons
|
||||
visuelles sont des volumes natifs de roche, sans textures, arbres ou structures,
|
||||
et ne remplacent pas l’essai du rendu complet dans Minecraft.
|
||||
|
||||
## Méthode
|
||||
|
||||
Le témoin principal est **Moyen 724, graine 0**. Le test compare le champ
|
||||
30.1 au champ retouché aux mêmes coordonnées. Il exporte deux coupes et un
|
||||
volume de **64 × 128 × 64 blocs**, choisi uniquement selon les transitions du
|
||||
champ précédent. Le choix du volume ne recherche pas le meilleur résultat de
|
||||
la nouvelle passe. La profondeur sous Y140 et l’arrivée restent inchangées.
|
||||
|
||||
Le volume permet de compter les composants de roche connectés par leurs six
|
||||
faces. Les composants touchant ses plans de coupe sont exclus de l’inventaire
|
||||
des fragments, car leur attache peut continuer hors du volume. Les longues
|
||||
marches sont comptées sur les surfaces supérieures complètes, le long des deux
|
||||
axes : suites d’au moins six différences de hauteur identiques de ±1 bloc.
|
||||
Ces mesures portent sur cet échantillon, pas sur toutes les graines du jeu.
|
||||
|
||||
Deux essais de déplacement plus simple ont été écartés avant publication : le
|
||||
premier introduisait trop de fragments, le second ne réduisait pas les longues
|
||||
marches du témoin. Les aperçus intermédiaires restent dans les dossiers ignorés
|
||||
`build/alpha302-preview-first-pass/` et `build/alpha302-preview-second-pass/`.
|
||||
|
||||
## Contrôle ciblé des fragments
|
||||
|
||||
`surfaceRelief302Smoke` vérifie les petits amas à cheval sur un bord de chunk,
|
||||
la conservation des corps connectés plus grands, l’absence de remplissage du
|
||||
vide, l’isolation des caches entre deux samplers et la limite de déplacement
|
||||
local. Le filtre s’applique au champ de roche retouché, avant les décorations ;
|
||||
il ne modifie pas les constructions ni les sauvegardes personnelles.
|
||||
|
||||
## Témoin Moyen 724, graine 0
|
||||
|
||||
Le processus **67135** passe les **24/24 GameTests** ; `check build` réussit
|
||||
en **5 min 18 s**. Reçus dans `build/alpha302-evidence/medium-0/`.
|
||||
|
||||
Dans le volume natif choisi (**X-154, Y164, Z-218**, 64 × 128 × 64), **96,72 %**
|
||||
des positions restent identiques. La quantité de roche passe de **174 157 à
|
||||
173 881 blocs**, soit une différence de **-0,16 %**. Les pas appartenant aux
|
||||
longues suites de marches passent de **424 à 81**. Le comptage indépendant
|
||||
observe **0 petit composant isolé de huit blocs ou moins**, contre cinq
|
||||
composants réunissant six blocs avant la passe. L’analyse ne compte pas les
|
||||
composants coupés par la frontière du volume. Le filtre lui-même traite les
|
||||
composants de 32 blocs ou moins.
|
||||
|
||||
Deux sorties de l’ancienne rivière sont vérifiées sous ticks réels :
|
||||
**-105 246 -65** et **-24 246 -45**. La traversée possède **19 départs natifs**,
|
||||
**29 chunks FULL** contrôlés, **272 blocs** entre ses accès et **678 pas** dans
|
||||
le parcours testé. Entrées : **-136 234 82** et **136 236 82**. Le sanctuaire
|
||||
possède son départ et son sol central ; les quatre satellites et une mine
|
||||
vanilla sont également observés dans leurs tests respectifs.
|
||||
|
||||
Les mesures de planification sont distinctes du temps de rendu client : la
|
||||
région hydrologique centrale prend **32,1 s**, et la recherche de traversée,
|
||||
qui inclut la préparation de son terrain, **40,8 s**. Ces durées ne s’ajoutent
|
||||
pas simplement. Le filtre a un coût ; cette livraison ne revendique pas un
|
||||
gain de performance. Le test d’export exhaustif du volume bloque ponctuellement
|
||||
le serveur de test ; cet export n’est pas activé dans le mod livré.
|
||||
|
||||
## Témoin Grand 1 024, graine -7228211907433324401
|
||||
|
||||
Le processus **67379** passe les **24/24 GameTests** en **4 min 23 s** pour la
|
||||
commande complète. Le volume natif contient **0 composant détaché de 32 blocs
|
||||
ou moins**, contre **12 avant**. Le test exige de ne pas augmenter ce nombre ;
|
||||
**1 024 points** rééchantillonnés dans l’ordre inverse, avec un autre contexte de
|
||||
cache, conservent le même état roche/air.
|
||||
|
||||
La traversée est réellement posée : **21 départs**, **48 chunks FULL**,
|
||||
**340 blocs entre les accès** et **574 pas** sur le parcours testé. Entrées :
|
||||
**192 238 -195** et **192 224 145**. Le sanctuaire et les quatre satellites
|
||||
passent leurs contrôles. La région centrale admet ici une rivière courbe ;
|
||||
l’écoulement des brèches est contrôlé sous ticks réels dans huit chunks FULL.
|
||||
|
||||
Reçus : `build/alpha302-evidence/large-user/`. Les deux commandes natives
|
||||
activent `sanctuaryAerial24Tests` et `sanctuaryTransit29Tests` ; le témoin Moyen
|
||||
exige aussi `sanctuaryRequireCurvedRiverBank=true`. Les trois tailles conservent
|
||||
leurs profils et le même filtre. Petit n’a pas fait l’objet d’une nouvelle
|
||||
visite native complète dans cette livraison. Les sauvegardes personnelles
|
||||
ne sont pas régénérées ; les scénarios natifs utilisent des mondes jetables.
|
||||
|
||||
Les reçus de distribution sont consignés dans [packwiz](packwiz.md).
|
||||
|
||||
## Distribution
|
||||
|
||||
`assemblePack` passe après la vérification complète ; `:sanctuary:check` est
|
||||
exclu de cette seconde commande pour ne pas répéter la suite inchangée. Le JAR,
|
||||
le MRpack et le ZIP d’amorçage sont vérifiés localement : versions, index,
|
||||
empreintes et contenu concordent. Reçus : `build/alpha302-artifacts.json` et
|
||||
`build/alpha302-delivery-local.json`. Les reçus publics et la mise à jour Prism
|
||||
sont consignés dans [Distribution](packwiz.md).
|
||||
|
||||
La release est publiée et ses trois artefacts sont revérifiés par téléchargement.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
Prism sur Mac réussissent, jeu fermé. Les **925 fichiers personnels et réglages
|
||||
suivis** gardent leurs empreintes. Les commits et sommes sont consignés dans
|
||||
[packwiz](packwiz.md#alpha302-publiée--12-septembre-2026).
|
||||
@@ -1,105 +0,0 @@
|
||||
# Vérification alpha.30.3
|
||||
|
||||
## Premier témoin natif — Mac
|
||||
|
||||
Monde Moyen 724, graine `0`, processus `68244` : 24/24 GameTests réussis.
|
||||
La comparaison utilise les densités natives enregistrées, pas un terrain simulé.
|
||||
La correction finale de bornage numérique à -1, sans effet sur les blocs solides,
|
||||
a ensuite été incluse dans le build de livraison.
|
||||
|
||||
| Profil mesuré (même graine) | Hauts terrains proches de la base 24* | Plus haut échantillon |
|
||||
|---|---:|---:|
|
||||
| Petit 512 | 330 / 463 (71 %) | Y344 |
|
||||
| Moyen 724 | 913 / 1 288 (71 %) | Y344 |
|
||||
| Grand 1 024 | 1 930 / 2 853 (68 %) | Y344 |
|
||||
|
||||
*Colonnes échantillonnées tous les 12 blocs, ancienne surface au-dessus de Y200,
|
||||
écart final de deux blocs au plus. La base de comparaison est le relief 24 repris
|
||||
en 27. Cela ne mesure ni la surface constructible exacte ni une qualité esthétique.
|
||||
Les trois profils sont compilés avec le même état aléatoire natif ; seul le
|
||||
monde Moyen de ce premier essai est parcouru en chunks FULL.
|
||||
|
||||
Le volume local contrôlé passe de cinq petits composants détachés en 30.1 à
|
||||
zéro en 30.3. Le parcours inversé des échantillons donne les mêmes appartenances.
|
||||
La densité sous Y170 est identique à la 30.1 aux points vérifiés.
|
||||
|
||||
La traversée a 19 départs de structure, 29 chunks FULL visités, portée 272.
|
||||
Le sanctuaire minéral est admis et son sol natif est inspecté. Les deux navires
|
||||
et deux îlots sont générés ; les tests du Blocodex et des minerais réussissent.
|
||||
La rivière courbe centrale est présente. Ce témoin ne fournit pas de nouvelle
|
||||
brèche de berge admissible : il ne constitue donc pas une preuve supplémentaire
|
||||
de cascade de surface sous ticks, malgré le succès du test des cellules d’eau.
|
||||
|
||||
Preuves ignorées : `build/alpha303-native-medium.log`,
|
||||
`build/alpha303-evidence/medium-0/`, `build/alpha303-preview/`.
|
||||
Les cartes et coupes visualisent la roche seule ; aucune capture client ni
|
||||
validation esthétique du joueur n’est revendiquée.
|
||||
|
||||
## Build de livraison et témoin Petit — Mac
|
||||
|
||||
`./gradlew check build -PsanctuaryTestDiameter=512 -PsanctuaryTestSeed=42
|
||||
-PsanctuaryTransit29Tests=true -PsanctuaryAerial24Tests=true` : réussite,
|
||||
24/24 tests natifs et contrôles JVM, 4 min 53 au total sur cette machine Mac.
|
||||
Ce temps comprend compilation, anciens tests et inspections ; ce n’est pas
|
||||
une mesure du délai de chargement d’un client.
|
||||
|
||||
Processus `69553`. Les 15 modules de traversée possèdent leurs départs natifs,
|
||||
32 chunks FULL sont parcourus, portée 204. Le sanctuaire minéral apparaît
|
||||
réellement dans le marais. Quatre sites aériens, quatre coffres, deux villageois,
|
||||
une source de cascade sur un îlot ; leurs coffres sont ouverts et les habitants
|
||||
déplacés dans ce monde jetable par le test. Aucun petit composant détaché dans
|
||||
le volume contrôlé ; 1 536 échantillons restent identiques en ordre inversé.
|
||||
|
||||
Les trois profils sous la graine 42 conservent 60 %, 59 % et 58 % des hauts
|
||||
terrains à deux blocs au plus de la base 24. Sommets échantillonnés :
|
||||
Y344 / Y346 / Y346. Une brèche de bassin produit de l’eau sous ticks réels ;
|
||||
la région centrale de ce témoin n’admet pas de rivière courbe.
|
||||
Preuves : `build/alpha303-check-build.log`, `build/alpha303-evidence/small-42/`.
|
||||
|
||||
## Témoin Grand signalé par le créateur — Mac
|
||||
|
||||
Grand 1024, graine `-7228211907433324401`, processus `69750` : 24/24 tests
|
||||
natifs réussis. La traversée compte 23 départs, portée 374, avec 52 chunks FULL
|
||||
parcourus et des accès vérifiés. Localisation témoin : `-187,249,-72`.
|
||||
Le sanctuaire minéral a un départ valide et un véritable sol central ;
|
||||
les quatre lieux aériens génèrent leurs quatre coffres et deux marchands.
|
||||
|
||||
La rivière courbe centrale est présente ; huit cellules d’eau sont constatées
|
||||
sous les ouvertures après propagation native. Le volume local passe de 12
|
||||
petits composants détachés à zéro. Les1 536 points contrôlés en ordre inversé
|
||||
restent identiques et les 7 436 échantillons profonds sont inchangés.
|
||||
|
||||
Sur cette graine, les plus hauts échantillons des profils 512/724/1024 atteignent
|
||||
Y322 / Y344 / Y346. L’altitude n’est pas forcée à 360 sur toutes les graines.
|
||||
Preuves : `build/alpha303-native-large.log`, `build/alpha303-evidence/large-user/`.
|
||||
Les fichiers `verified-run.json` désignent les rapports de chaque processus ;
|
||||
les autres anciens diagnostics copiés ne comptent pas comme des essais 30.3.
|
||||
|
||||
## Défauts corrigés pendant la vérification
|
||||
|
||||
Petit 512, graine 42, révélait deux défauts distincts :
|
||||
|
||||
- La recherche aérienne épuisait ses colonnes sur des emprises trop hautes ou
|
||||
trop creuses. Le rejet de hauteur est maintenant immédiat, l’appui est filtré
|
||||
grossièrement avant certification et les candidats de repli couvrent le tour
|
||||
de l’île. Le plan témoin obtenu emploie 8 058 colonnes, avec quatre sites.
|
||||
- La chambre minérale était admise en `sanctuary:swamp27_caves`, mais ce biome
|
||||
manquait au tag `has_structure/forgotten_networks`. Le départ natif était donc
|
||||
refusé. Le tag inclut désormais ce biome, sans contourner la certification.
|
||||
|
||||
Les premiers journaux en échec sont conservés sous `build/alpha303-small42-*failure.log`.
|
||||
|
||||
## Limites
|
||||
|
||||
Essais exécutés sur Mac ; aucun essai natif Windows par l’agent. Les variations
|
||||
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).
|
||||
@@ -1,90 +0,0 @@
|
||||
# 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 reçus publics et l'installation Prism seront consignés 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.
|
||||
@@ -1,150 +0,0 @@
|
||||
# Alpha.30 — vérifications
|
||||
|
||||
Essais du 12 septembre 2026 sur **Mac**, dans des serveurs de développement
|
||||
sans rendu client. Le retour Windows du créateur concerne la 28 ; aucun essai
|
||||
Windows natif de la 30 n’est revendiqué. Nouveau monde nécessaire.
|
||||
|
||||
## Témoin Grand signalé par le créateur
|
||||
|
||||
**1 024 blocs, graine `-7228211907433324401`**, processus 62002 :
|
||||
**24/24 GameTests passent**. Reçus dans `build/alpha30-evidence/grand-user/`.
|
||||
|
||||
- Traversée réellement générée : 23 modules, 52 chunks FULL, 374 blocs entre
|
||||
les accès ; parcours contrôlé sur 736 pas et 71 points, avec les collisions
|
||||
natives, escaliers et raccords. Entrées : **192 238 -195** et **192 256 179**.
|
||||
- Rivière : tracé entre des points distants d’environ **865 blocs**, 6 924 cellules
|
||||
de lit et de brèche, six ouvertures. Huit chunks FULL échantillonnés ; après
|
||||
120 ticks, six échantillons contiennent de l’eau sous les deux sorties suivies.
|
||||
Ce test ne prétend pas simuler toutes les chutes jusqu’à Y0.
|
||||
- Sanctuaire minéral accepté dès la recherche intérieure ; emprise depuis
|
||||
**-122 131 265**, 79 × 73 × 27. Le recours exposé n’est pas exercé sur ce témoin.
|
||||
- Quatre satellites, deux villageois, quatre coffres et une cascade d’îlot :
|
||||
44 chunks FULL contrôlés. Les témoins de pillage, déplacement et démolition
|
||||
sont préparés pour le contrôle de persistance.
|
||||
- Le petit échantillon de structures vanilla ne contient pas de mine. Ce n’est
|
||||
pas un inventaire exhaustif de l’île et aucune présence de mine n’y est annoncée.
|
||||
|
||||
## Petit 512, graine 2026
|
||||
|
||||
Processus **62463**, **24/24 GameTests passent**. Reçus :
|
||||
`build/alpha30-evidence/small-2026-final2/`.
|
||||
|
||||
La traversée produit 15 modules et leurs départs natifs, avec 204 blocs entre
|
||||
les entrées. Ce contrôle est limité aux plans et départs ; aucun parcours FULL
|
||||
complet de cette traversée n’est revendiqué sur Petit. La rivière couvre une
|
||||
distance d’environ 362 blocs, avec 2 551 cellules et quatre brèches. Sept chunks
|
||||
FULL sont échantillonnés ; l’eau coule sous les sorties contrôlées.
|
||||
|
||||
Le sanctuaire possède son véritable départ de structure et son sol central
|
||||
est contrôlé après génération au point **-54 162 69**, avec l’espace libre
|
||||
au-dessus. Les quatre satellites passent sur 41 chunks FULL.
|
||||
|
||||
Un premier essai a révélé que le témoin de vue de l’îlot à cascade se trouvait
|
||||
sur une ancienne hauteur creusée en bassin. La correction choisit une berge
|
||||
sèche proche, sans ajouter de support artificiel. Une première correction trop
|
||||
large épuisait le budget de colonnes ; elle a été remplacée avant validation.
|
||||
La commande finale dure 2 min 8 s.
|
||||
|
||||
## Moyen 724, graine 0
|
||||
|
||||
Processus **62604**, reçus `build/alpha30-evidence/medium-0/`.
|
||||
La traversée réelle passe : **19 modules, 42 chunks FULL, 272 blocs entre les
|
||||
accès, 456 pas et 62 points accessibles**. Entrées : **-136 248 -53** et
|
||||
**136 260 -53**. La rivière s’étend sur environ 554 blocs : 3 942 cellules,
|
||||
six brèches, neuf chunks FULL contrôlés et écoulement sous les sorties suivies.
|
||||
Le sol central du sanctuaire est présent en **106 189 -63**, avec l’espace
|
||||
libre au-dessus ; les satellites passent également.
|
||||
|
||||
Le test exigeant une mine sur cette graine échoue : aucun départ de mine n’est
|
||||
retenu parmi ses 19 candidats, y compris via la recherche native. Les **23 autres
|
||||
tests passent**. Le témoin de mine de la 29 n’est donc pas réutilisé comme preuve
|
||||
de présence en 30. Un témoin séparé est recherché, sans forcer une implantation
|
||||
incompatible. Les refus conditionnels restent un comportement voulu du jeu.
|
||||
|
||||
## Grand 1 024, graine 42 — témoin de mine
|
||||
|
||||
Processus **62979**, **24/24 GameTests passent**, y compris l’exigence de trouver
|
||||
une mine réelle. Reçus dans `build/alpha30-evidence/grand-42/`.
|
||||
|
||||
Deux départs de mines vanilla sont observés dans les candidats examinés. La
|
||||
mine visitée possède **83 pièces** ; l’échantillon de 16 chunks FULL du test
|
||||
contient notamment **138 planches de chêne, 120 barrières, 43 rails et 23 toiles**.
|
||||
Point de visite : **6 189 6**. Le test contrôle les pièces sauvegardées et leur
|
||||
aller-retour NBT ; il ne visite pas l’intégralité de la mine et ne garantit pas
|
||||
un spawner dans chaque corridor. Une lecture séparée de la sauvegarde confirme
|
||||
un véritable spawner `cave_spider` en **46 188 25** dans un corridor :
|
||||
`build/alpha30-evidence/grand-42/saved-spawners.json`. Son activation par un joueur
|
||||
n’a pas été testée.
|
||||
|
||||
Deux huttes de sorcière sont également observées. La hutte visitée est en
|
||||
**83 148 -396**, avec ses pièces, appuis et son mobilier vanilla.
|
||||
La traversée possède **25 modules répartis sur 24 départs natifs**, avec 374 blocs entre
|
||||
les entrées ; cette graine utilise le contrôle des plans, sans nouveau parcours
|
||||
FULL de toute la traversée. Le sanctuaire possède son vrai départ et son sol
|
||||
central. La rivière s’étend sur environ **882 blocs**, avec 5 822 cellules et
|
||||
six ouvertures ; les écoulements suivis passent sous ticks réels.
|
||||
|
||||
`./gradlew check build` passe sur ce témoin : **88 tâches**, dont les tests purs,
|
||||
les contrôles de sérialisation et les **24 GameTests natifs**, en **6 min 28 s**.
|
||||
Le coût inclut toute la suite et n’est pas un temps de chargement de partie.
|
||||
Commande :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryTestDiameter=1024 -PsanctuaryTestSeed=42 \
|
||||
-PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true \
|
||||
-PsanctuaryTransit29Tests=true -PsanctuaryTransit29PlanOnly=true \
|
||||
-PsanctuaryRequireMineshaft29=true
|
||||
```
|
||||
|
||||
## Relief
|
||||
|
||||
Sur ce Grand, 8 112 densités sous Y180 sont identiques bit à bit à la 28.
|
||||
Dans les coupes hautes, 4 544 / 94 640 échantillons changent de signe, soit
|
||||
**4,80 %**. Deux coupes au pas d’un bloc, Z0 et Z96, ont été rendues et examinées :
|
||||
les plateaux redeviennent larges entre les zones où subsistent les surplombs.
|
||||
Aperçu local `build/alpha30-preview/relief-1024--7228211907433324401.png`.
|
||||
Les mêmes coupes ont aussi été examinées sur Petit2026 : 602 / 40 460 signes
|
||||
modifiés (1,49 %), avec des sommets plus plats entre les zones déformées.
|
||||
Ce contrôle de densité ne remplace pas une exploration visuelle dans le client
|
||||
et ne démontre pas l’absence de toute petite pointe ou masse déconnectée.
|
||||
|
||||
## Tests ciblés et limites
|
||||
|
||||
Le test pur `river30Smoke` couvre trois rayons sur une masse rocheuse synthétique :
|
||||
longueur du tracé, lit dans la roche, arrivée protégée, brèches de cinq blocs
|
||||
maximum, déterminisme et espaces non déformés entre les trois secteurs.
|
||||
Ce n’est pas une matrice de neuf mondes Minecraft natifs.
|
||||
|
||||
La recherche reste bornée par la taille de l’île et le nombre de candidats.
|
||||
Sur le Grand, la commande native complète dure **4 min 44 s**, incluant le
|
||||
lancement, la planification, les structures et les nombreux chunks demandés
|
||||
par les tests. Ce temps n’est ni un temps de rendu client, ni un benchmark
|
||||
comparatif avec la 28. Aucun objectif chiffré de performance n’est revendiqué.
|
||||
|
||||
Les tests vérifient des occurrences et parcours précis, pas une garantie
|
||||
universelle sur toutes les graines. Les mines restent conditionnelles, et le
|
||||
sanctuaire peut encore être refusé si aucune implantation n’a d’attache valable.
|
||||
Les réglages, le Blocodex et la suite générale restent soumis à `check build`.
|
||||
|
||||
## Redémarrage et artefacts
|
||||
|
||||
Grand1 024, graine42, processus **63276** : **24/24 tests passent après un
|
||||
redémarrage réel** (2 min 14 s). Les 47 chunks FULL du contrôle aérien conservent
|
||||
coffres pillés, villageois déplacés et destructions préparées à la création,
|
||||
sans duplication ni reconstruction des témoins. Les départs de mine et leurs
|
||||
pièces, le sol du sanctuaire et les écoulements échantillonnés passent à nouveau.
|
||||
Le journal d’expansion reste inchangé. Preuves :
|
||||
`build/alpha30-evidence/grand-42-reload/` et `build/alpha30-reload.log`.
|
||||
|
||||
`./gradlew assemblePack` passe après `check build` (le second lancement exclut
|
||||
la tâche `:sanctuary:check` déjà réussie). Le JAR, le MRpack autonome et le ZIP
|
||||
d’amorçage Prism passent la vérification locale : versions, dépendances exactes,
|
||||
index et empreintes concordent. Reçus : `build/alpha30-artifacts.json`,
|
||||
`build/alpha30-delivery-local.json`. Les fichiers du client Windows n’ont pas
|
||||
été modifiés. La publication et la synchronisation de l’instance Mac sont
|
||||
suivies dans [la distribution](packwiz.md).
|
||||
|
||||
La release alpha.30 est publiée depuis `bf327b7`. Les archives publiques ont les
|
||||
mêmes empreintes que les fichiers vérifiés ; le canal et l’instance Prism Mac
|
||||
sont synchronisés deux fois. Les 925 fichiers personnels suivis sont inchangés.
|
||||
Les reçus de distribution et empreintes sont dans [packwiz](packwiz.md).
|
||||
-1275
File diff suppressed because it is too large
Load Diff
-538
@@ -1,538 +0,0 @@
|
||||
# Sanctuary — vision du projet
|
||||
|
||||
Ce document conserve les intentions exprimées au démarrage de Sanctuary Beta. Il décrit une **destination de conception**, pas une liste de fonctionnalités déjà livrées. Le code, les tests et les notes de version font foi pour l'état réel du mod. Les valeurs d'équilibrage ci-dessous sont des propositions initiales à éprouver en jeu.
|
||||
|
||||
La cible demandée est **Minecraft 26.3 avec Fabric**. Au démarrage du dépôt, le 8 septembre 2026, la base disponible retenue est **26.3-pre-2** ; les versions effectivement utilisées restent indiquées dans la configuration de construction. La première étape est d'initialiser proprement le projet, puis de retrouver et d'adapter les idées pertinentes du générateur de l'ancienne version 26.2.
|
||||
|
||||
## Le projet en quelques mots
|
||||
|
||||
> Sanctuary is a free expansion of Minecraft that reshapes the game around a floating world. Players begin together on Sanctuary Island, isolated in the void. By exploring, building infrastructure and producing resources, they progressively unlock new floating continents, each with its own geography. Rather than a linear campaign, the server itself becomes a world that grows through the action of its players.
|
||||
|
||||
Sanctuary transforme Minecraft en un monde flottant d'exploration, de production et de progression collective. Tous les joueurs commencent sur l'île de Sanctuary, isolée dans le vide. Leurs constructions, leurs explorations et leurs chaînes de production permettent d'ouvrir de nouveaux continents suspendus.
|
||||
|
||||
L'objectif mythologique est de progresser d'une condition très limitée, proche du hardcore, vers les possibilités du mode créatif, par la coopération. Le serveur conserve l'histoire de cette transformation : les continents ouverts, les ressources produites, les événements, les étoiles et les constellations. Battre l'Ender Dragon devient un événement parmi d'autres, et non la conclusion obligatoire d'une campagne.
|
||||
|
||||
## Principes de conception
|
||||
|
||||
- **Un monde construit par ses habitants.** Les efforts collectifs doivent avoir des effets durables et visibles sur le serveur.
|
||||
- **Des objets et des gestes dans le monde.** Favoriser panneaux, ordinateurs, cloches, bannières, télescopes et infrastructures pour accéder aux systèmes. Ajouter un menu quand il sert réellement l'interaction.
|
||||
- **Coopération et initiative individuelle.** Permettre les projets communs sans effacer les possessions, les collections, les voisinages ni les constructions personnelles.
|
||||
- **Une progression par l'expérience et la production.** L'XP sert aux capacités, aux transports et aux actions créatives ; les ressources et les métiers soutiennent l'expansion.
|
||||
- **Un monde aux ressources localisées.** Il faut partir explorer pour trouver certains biomes, cultures, structures et matériaux.
|
||||
- **Humour, surprise et souvenirs.** Les noms d'entités, les événements, les animaux rares et les objets insolites comptent autant que les systèmes économiques.
|
||||
- **Un développement par tickets.** Livrer une petite mécanique jouable et vérifiable, puis l'ajuster avec des tickets de fonctionnalités et de bugs. Ne pas construire tous les systèmes avant de pouvoir jouer.
|
||||
|
||||
## Minecraft comme archéologie d’un autre Minecraft
|
||||
|
||||
**Canon et ligne de conception retenus.** Steve, Ari, Sunny, Kai, Zuri, Alex,
|
||||
Mefe, Makena, Noor sont les personnages disparus de la partie. Ils ont découvert
|
||||
quelque chose de révolutionnaire et ont continué parce que ça marchait. Jusqu’au
|
||||
moment où ça n’a plus marché.
|
||||
|
||||
> Ils n’ont pas ouvert quelque chose qu’ils n’auraient pas dû ouvrir. Ils ont
|
||||
> construit quelque chose qu’ils ne savaient plus refermer.
|
||||
|
||||
Chaque ruine doit être une trace d’une ancienne solution, pas seulement la trace
|
||||
d’un problème : une infrastructure dont l’auteur a progressivement disparu
|
||||
derrière son fonctionnement. Son ancien usage, ses circulations, ses réparations,
|
||||
ses extensions et ses chantiers doivent permettre de comprendre ce qui avait
|
||||
été résolu ici, même quand l’installation est abandonnée.
|
||||
|
||||
La nature de la découverte et les rôles individuels restent à définir. Aucune
|
||||
catastrophe précise n’est confirmée par ce principe. **La mise en scène de cette
|
||||
histoire et ses liens avec la progression restent à implémenter** ; les versions
|
||||
livrées et leurs tests établissent ce que chaque structure contient effectivement.
|
||||
|
||||
Les **Indoors** prolongent cette histoire comme espaces artificiels intentionnels,
|
||||
référencés et contrôlés. Les **Backrooms** en sont les espaces orphelins : la
|
||||
machine tente d’organiser de la matière dont elle a perdu le contexte. Le ballast
|
||||
produit par les échanges, déplacements et créations d’espaces doit pouvoir y
|
||||
inscrire le comportement réel des joueurs, jusqu’à rendre l’histoire économique
|
||||
du serveur lisible dans leur géologie. Le [cahier de cosmologie](cosmologie.md)
|
||||
conserve le texte de l’auteur, ses exemples et les articulations encore à définir.
|
||||
**Ces espaces, le shop et le ballast ne sont pas implémentés.** Le socle méta
|
||||
des blocs pourra les alimenter ; il ne constitue pas encore un journal économique.
|
||||
|
||||
## Périmètre de départ
|
||||
|
||||
Le premier incrément porte sur le socle Fabric et la génération du monde : retrouver le générateur 26.2, produire une île principale dans un vrai vide, assurer une arrivée commune et sûre, puis poser une manière reproductible de générer des continents à une distance, une direction et une taille choisies.
|
||||
|
||||
La progression, l'économie, les nouveaux monstres, les interfaces de pack et les autres dimensions restent des intentions futures. Ils ne constituent pas des dépendances du premier générateur. Le [backlog](backlog.md) précise les premiers tickets et leurs critères d'acceptation.
|
||||
|
||||
L’alpha se poursuit vers les versions 15 ou 16 pour éprouver la génération et
|
||||
ses structures. La bêta véritable commencera ensuite les systèmes de Sanctuary,
|
||||
en particulier la progression. Ce cap décrit un ordre de travail : une version
|
||||
n’est pas considérée terminée avant ses essais et sa livraison.
|
||||
|
||||
## Mod, modpack et modules autonomes
|
||||
|
||||
| Ensemble | Rôle souhaité |
|
||||
| --- | --- |
|
||||
| **Sanctuary, le mod** | Monde flottant, expansion, progression collective et systèmes propres à cet univers. |
|
||||
| **Sanctuary, le modpack** | Distribution avec packwiz, configurations, mods communautaires, resource packs et shader packs. |
|
||||
| **It's Alive !** | Mod de cuisine et d'agriculture autonome, raccordable aux biomes, événements et déblocages de Sanctuary. |
|
||||
| **Only Fun** | Mod autonome consacré aux interactions humoristiques, aux anniversaires et à des activités sociales. |
|
||||
| **Master Key** | Bibliothèque d'outils et de permissions d'administration destinée aux différents mods, afin d'administrer et de réparer sans devoir jouer en créatif. |
|
||||
|
||||
L'apparence du pack peut inclure les mentions affichées, les crédits, l'icône, l'écran de chargement et des adaptations visuelles. Les licences et attributions des composants restent attachées à leurs ayants droit ; choisir une licence pour le travail original est une décision distincte de personnaliser son habillage.
|
||||
|
||||
Les intégrations communautaires envisagées comprennent Fabric API, Sodium, Iris, Vitrail, Indium, Just Enough Items, Simple Voice Chat, Xaero's Minimap et Dynamic Torches. Golden Days est envisagé pour l'apparence du monde Alpha, et Litematica pour les plans de construction. Ce sont des pistes d'intégration : la disponibilité sur la version cible, les noms exacts, les dépendances et les conditions de distribution doivent être vérifiés au moment de chaque ticket.
|
||||
|
||||
**TerraMix** désigne ici le catalogue natif de 100 biomes d'**Another World**, retrouvé dans l'historique 26.2. Sa reprise et son extension sont une piste pour les continents ; ce nom ne désigne pas une nouvelle dépendance externe obligatoire.
|
||||
|
||||
## Monde flottant et expansion
|
||||
|
||||
### Sanctuary Island et continents
|
||||
|
||||
L'île de Sanctuary est le point de départ commun. Elle flotte dans un vide qui ne doit pas être rempli par la génération habituelle du monde. Les continents suivants sont de vastes terres suspendues, créées progressivement selon une taille et une direction maîtrisées.
|
||||
|
||||
Le monde doit se créer par un seul choix **Sanctuary**, avec le bouton natif
|
||||
**Personnaliser** pour régler sa taille : **Petit** (512 blocs de diamètre
|
||||
nominal), **Moyen** (724, par défaut) et **Grand** (1 024). La hauteur reste de
|
||||
384 blocs. Ces dimensions ne promettent ni une aire entièrement habitable,
|
||||
ni un stock de ressources, ni une capacité multijoueur.
|
||||
|
||||
L’alpha.13 implémente cette interface et la réunion de l’île complète de Population,
|
||||
de ses forêts et de son hydrologie, avec les cavités natives du laboratoire.
|
||||
L’alpha.11 et ses anciens choix Petit/Grand restent historiques. Les anciens
|
||||
codecs et paramètres sauvegardés restent chargeables ; les choix précédents
|
||||
sont cachés à la création et aucune partie n’est convertie ou redimensionnée.
|
||||
L’état d’intégration est décrit dans le [guide alpha.13](alpha13-worldgen.md) ;
|
||||
les observations réelles et leurs limites restent dans [Validation](testing.md).
|
||||
|
||||
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
|
||||
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
|
||||
pierre et terre enherbée. De petits étangs ou lacs de surface
|
||||
peuvent accueillir de la canne à sucre. Lorsque le relief le permet, une grande
|
||||
rivière suit la forme de l’île, depuis une source jusqu’à une cascade et un
|
||||
bassin, avec des rives de sable, de gravier et d’argile. Ce parcours doit former
|
||||
un lieu reconnaissable ; il reste facultatif. À défaut, l’île conserve ses
|
||||
petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près de la surface,
|
||||
avec des dépôts épais et des rives variées. De rares sources sortent de niches
|
||||
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
|
||||
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
|
||||
Une rivière peut avoir des échappements et des chutes déclarés. Les eaux
|
||||
intérieures doivent rester localisées et retenues, sans aquifère qui remplit
|
||||
globalement les cavités. Des bassins naturels étagés, évoquant des rizières,
|
||||
peuvent se relier par des cascades et offrir des passages verticaux par la nage.
|
||||
Cette image ne prévoit aucune culture de riz dans le mod Sanctuary.
|
||||
Le terrain doit laisser voir un dessous sculpté et dégagé, dans un monde de
|
||||
**384 blocs de hauteur**, avec des
|
||||
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
|
||||
|
||||
L’ambiance de départ doit rester douce et tempérée, avec de véritables forêts
|
||||
et des clairières fleuries, quelques zones plus sèches et de larges masses
|
||||
rocheuses mêlant pierre, andésite, diorite et granite. Les nappes de sable et
|
||||
de gravier accompagnent les eaux. Charbon, fer et cuivre doivent aussi pouvoir
|
||||
affleurer sur la roche exposée pour commencer la survie, avec du charbon assez
|
||||
repérable pour fournir les premières expériences. Les failles doivent ouvrir
|
||||
des passages courbes et des vues vers les profondeurs, sans reconstruire une
|
||||
masse artificielle au centre de l’île.
|
||||
|
||||
La géologie change en descendant : tuf, cobblestone, pierres moussues et boue
|
||||
compactée dans les intérieurs humides, puis ardoise des abîmes, roche noire et
|
||||
basalte. Des poches de soufre doivent être réellement explorables et rester
|
||||
ponctuelles, avec de vrais geysers actifs lorsque les conditions s’y prêtent.
|
||||
Quelques sources profondes peuvent former des coulées de lave ; elles ne doivent pas
|
||||
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
|
||||
minerais à rechercher, avec l’or, le diamant, la redstone et le lapis.
|
||||
|
||||
Les bois dominants sont le chêne, le chêne noir et le bouleau : chênes et
|
||||
bouleaux en surface, chênes noirs et gros champignons dans l’intérieur plus
|
||||
humide, avec de petits champignons dispersés et des tapis variés de mousse,
|
||||
podzol et mycélium. Épicéa, jungle, acacia, palétuvier et chêne pâle restent rares
|
||||
mais possibles ; du bambou peut se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un
|
||||
cerisier rose ou une autre de ces essences rares, lorsque le terrain convient.
|
||||
Les biomes très contrastés de Minecraft 26.3, dont Dappled Forest, restent
|
||||
réservés aux futurs continents.
|
||||
|
||||
Les cavités sous l’île doivent aussi accueillir des ambiances luxuriantes
|
||||
avec de la végétation lumineuse, des secteurs à stalactites et stalagmites et
|
||||
des géodes d’améthyste. Leur décor doit utiliser les sols et plafonds du relief,
|
||||
sans reconstruire l’île ni déposer de structures flottantes sans support.
|
||||
L’alpha.9 introduit ces ambiances et recherche des lacs plus profonds et
|
||||
volumineux ; les résultats et leurs limites sont documentés dans les contrôles moteur. Ces cavités appartiennent à
|
||||
l’Overworld de Sanctuary et ne remplacent pas le futur projet de dimension
|
||||
des cavernes.
|
||||
|
||||
L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft,
|
||||
en quantités modestes pour pousser à explorer les expansions lors des grands
|
||||
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
|
||||
stocks sont observés après génération et les ressources renouvelables gardent
|
||||
leurs règles Minecraft. Le réglage précis de ces quantités reste à éprouver en
|
||||
partie coopérative. La recherche de l’alpha.8 porte notamment sur une rivière
|
||||
calme continue reliant deux bassins au même niveau, alimentée par une source
|
||||
en paroi et sa cascade. Son tracé doit former de larges courbes, avec une
|
||||
largeur variable et des berges progressives. La petite réserve de lave est
|
||||
recherchée d’abord dans les étages profonds ; un site plus haut reste possible
|
||||
si la roche ne fournit pas de niche compatible en dessous. Ces formes restent
|
||||
facultatives selon le relief ; leur existence doit être démontrée en jeu.
|
||||
L’état de chaque incrément et ses limites
|
||||
figurent dans [Génération](worldgen.md).
|
||||
|
||||
La progression des expansions se fait par **choix en branchement**. La direction
|
||||
géographique, le climat et l’archétype sont des choix distincts. La convention
|
||||
**nord froid, sud chaud, ouest sec et est humide** reste une voie possible, avec
|
||||
ses diagonales. Le joueur peut aussi ouvrir une branche alternative et composer
|
||||
des couronnes de nouvelles îles aux climats différents. Une île enregistre son
|
||||
parent ; le serveur conserve ainsi l’histoire des découvertes de son univers.
|
||||
La recherche porte sur des climats et leurs distributions, sans imposer la même
|
||||
forme ni le même climat à une direction pour toutes les parties.
|
||||
|
||||
Les alpha.12 et alpha.12.1 ont fourni un laboratoire à cette conception :
|
||||
commandes opérateur, climats et reliefs indépendants, structures vanilla
|
||||
compatibles et recherche d’emplacements libres. L’alpha.13 reprend ces mêmes
|
||||
outils dans le nouveau monde Sanctuary unifié. Son journal est séparé de celui
|
||||
des anciens laboratoires, sans conversion de leur génération.
|
||||
Les recherches, mailboxes et deposit boxes viendront ensuite ; elles ne sont
|
||||
pas nécessaires aux commandes d’essai. Le passage en bêta dépendra du périmètre
|
||||
de progression retenu et de sa validation, pas du seul changement de générateur.
|
||||
|
||||
L'économie naturelle d'une île doit être connue **après sa génération** : nombre
|
||||
de blocs de pierre, bûches, minerais de charbon, de fer et autres ressources,
|
||||
avec une emprise et un état de référence précis. Ce relevé constate ce que la
|
||||
génération a produit ; il ne force pas des quotas de ressources et ne choisit pas
|
||||
une seed pour atteindre un stock prédéterminé. Un relevé partiel doit rester
|
||||
identifié comme tel. Le contrat de recherche et de relevé est détaillé dans
|
||||
[Expansion et ressources](expansion.md).
|
||||
|
||||
Leur géographie doit rester variée : perforations et passages de vide, océans et lacs retenus dans le terrain, rivières flottantes, montagnes, ravins et reliefs lisibles depuis les airs. Le système doit pouvoir conserver les constructions et les chunks déjà explorés lorsqu'une nouvelle expansion est ouverte.
|
||||
|
||||
L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue TerraMix d'Another World, avec des biomes originaux comme le **Black Desert**. Cette diversité sera introduite après la validation du terrain de base et de son adaptation technique.
|
||||
|
||||
### Structures et territoires
|
||||
|
||||
**Direction en conception après l’alpha.22 :** départ dans un endroit naturel,
|
||||
puis découverte de quelques lieux isolés. La sélection retient l’observatoire,
|
||||
la salle d’expansion, un atelier caché et la grande traversée, avec l’ajout à
|
||||
concevoir d’un donjon difficile, un boss et un objet de quête essentiel à la
|
||||
progression. Les villes, regroupements et autres structures restent en réserve.
|
||||
Les fonctions sont reproductibles chez les joueurs, y compris l’installation
|
||||
d’expansion ; quelques exceptions collectives restent à définir. La salle
|
||||
ancienne peut être rééquipée et servir de modèle. Voir le
|
||||
[cahier de conception en discussion](structures-conception.md).
|
||||
Ces décisions préparent la suite ; les paragraphes datés ci-dessous conservent
|
||||
l’historique des versions et ne signifient pas que cette nouvelle sélection est
|
||||
déjà implémentée.
|
||||
|
||||
- **Structures vanilla** : admises dans les nouvelles générations lorsque leur
|
||||
biome, leur emprise et leurs fondations conviennent au terrain flottant.
|
||||
L’autorisation ne garantit pas leur présence sur chaque île. Le laboratoire
|
||||
commence par les villages, temples, cabanes de marais et camps abandonnés.
|
||||
- **Lost City** : des quartiers traités comme un biome, où les bâtiments
|
||||
contemporains en ruine prennent la place principale. L’alpha.14 développe une
|
||||
réalisation originale Sanctuary, inspirée du principe de ville abandonnée,
|
||||
sans ajouter le mod The Lost Cities ni reprendre ses assets. L’alpha.16 garantit une petite ville compacte sur la nouvelle île de départ,
|
||||
lorsque les structures sont activées, puis admet des quartiers conditionnels
|
||||
dans les continents d’expansion, en laissant
|
||||
une majorité naturelle à l’île initiale. La pierre, la pierre lisse et les
|
||||
briques de pierre, notamment fissurées et moussues, donnent une palette sobre.
|
||||
- **Vie urbaine passée** : routes, ponts, places, marchés, appartements,
|
||||
bibliothèques, restaurants et cuisines abandonnées, hôtels et chambres,
|
||||
casernes, halls, excavations et piscines. Les parcs comprennent boisements,
|
||||
jardins à haies et fleurs, aires de jeux et mémoriaux. Chaque usage doit se
|
||||
reconnaître dans les volumes et les aménagements, avec entrées et circulations.
|
||||
La passe alpha.15 vise des pièces réellement séparées et aménagées : seuil,
|
||||
vestibule, hall, corridors, escaliers, chambres, sols, plafonds et ornements.
|
||||
Les rues, parcs et bâtiments doivent être incrustés dans le sol. Les bâtiments
|
||||
se regroupent en îlots denses avec rues et carrefours continus, sans mottes
|
||||
de terre entre les façades et leurs accès. Les ponts et viaducs prolongent
|
||||
le réseau urbain sur les franchissements naturels admissibles.
|
||||
- **Parkings et sous-sols** : creuser seulement là où l’épaisseur de roche
|
||||
permet un volume soutenu ; un garage en surface reste possible ailleurs.
|
||||
Les excavations ouvertes et les piscines doivent aussi garder des fondations,
|
||||
des accès et, pour l’eau, un bassin contenu. Les parkings ne font pas partie
|
||||
de la future dimension des cavernes. L’alpha.15 travaille des égouts et pool
|
||||
rooms en réseau, procéduralement creusés sous la ville,
|
||||
avec des passages et des accès de surface. Une section suspendue exige des
|
||||
chaînes ou des pylônes ancrés. Leur présence reste liée au terrain ; le
|
||||
suivi de validation distingue les intentions des preuves. L’alpha.16 ajoute
|
||||
un bassin couvert de 50 × 25 blocs, des plateformes de parking jusqu’à huit
|
||||
niveaux et des exutoires ouverts avec écoulement natif. Les étages alternent
|
||||
leurs plans, les sanitaires sont clos et les lits adossés aux murs. Seule la
|
||||
zone urbaine initiale est garantie ; piscine et autres grands équipements
|
||||
dépendent du terrain et des appuis. Voir [la validation alpha.16](testing-alpha16.md).
|
||||
- **Future salle d’expansion** : une grande salle sèche reliée aux tunnels
|
||||
pourrait accueillir une machine diégétique. Un socle et des cadres porteraient
|
||||
des cartes déjà découvertes, présentant l’ensemble de l’île en vue du dessus.
|
||||
L’alpha prépare la salle ; la machine, les cartes préremplies et les
|
||||
interactions d’expansion seront définies et implémentées dans un ticket suivant.
|
||||
- **Occupants de Lost City** : des zombies plutôt que des villageois, sans
|
||||
spawner. L’alpha utilise le spawn naturel des zombies vanilla ; les infectés,
|
||||
spitters, boomers, hunters, chargeurs et autres classes arrivent avec les
|
||||
systèmes de la bêta. La présence de zombies dépend des règles du jeu.
|
||||
- **Structures uniques et donjons** : d'abord sur Sanctuary Island, puis dans les continents. Ils peuvent contenir des clés de chunk et d'autres ressources rares.
|
||||
- **Sanctuary était habitée avant nous** (orientation validée après l’alpha.15) :
|
||||
développer les trois familles ensemble : mines et ateliers, lieux étranges,
|
||||
infrastructures à réactiver. Les ruines doivent raconter une occupation
|
||||
antérieure par d’autres joueurs, à travers leurs usages et leurs circulations.
|
||||
Plusieurs réseaux distincts peuvent se rejoindre ponctuellement : découvrir
|
||||
ces connexions fait partie de l’exploration. Privilégier les installations
|
||||
encore utilisables et les chantiers interrompus pour raconter cette histoire.
|
||||
Les back rooms de ces donjons sont des lieux physiques dans l’île, distincts
|
||||
des futurs espaces Backrooms recevant les objets perdus. Le futur butin combine
|
||||
des reliques collectives uniques et des récompenses personnelles ; les
|
||||
spawners pourront rester exploitables après exploration sans régénérer les
|
||||
donjons. Les règles précises restent à concevoir. Le réacteur et ces nouveaux
|
||||
systèmes ne font pas partie de l’alpha.16.
|
||||
Le prototype alpha.17 commence par trois petits réseaux indépendants, avec
|
||||
accès de surface, ateliers, galeries, salles étranges et locaux techniques.
|
||||
L’option « Structures expérimentales » se choisit pour un nouveau monde.
|
||||
Les connexions entre familles et les systèmes de butin spécifiques restent
|
||||
à construire ; voir [le contrat du prototype](testing-alpha17.md).
|
||||
- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane.
|
||||
|
||||
### Dimensions et espaces
|
||||
|
||||
| Monde ou espace | Intention |
|
||||
| --- | --- |
|
||||
| **Cavernes** | Monde destiné au minage, avec des biomes souterrains, sans mineshafts ni parkings. Des cités anciennes particulièrement difficiles accueillent plusieurs types de squelettes et un nécromancien illageois qui les invoque. Il détient une boule de cristal. |
|
||||
| **Alpha** | Monde inspiré de Minecraft Indev et des premières versions, avec les anciennes textures de Golden Days actives uniquement dans cette dimension. Notch y est un boss. |
|
||||
| **Backrooms** | Espaces artificiels orphelins dans la cosmologie, dont la géologie doit conserver l’histoire économique du serveur. L’intention antérieure d’un territoire sombre à plusieurs niveaux, accessible par le lit et recueillant des objets perdus, reste à articuler avec cette direction. |
|
||||
| **Indoors** | Espaces artificiels intentionnels, référencés et contrôlés : une porte peut mener à un intérieur plus grand que le bâtiment. L’auteur les décrit comme de petites dimensions instanciées. Les classes de volume envisagées vont indicativement de 5 × 5 × 5 à 100 × 100 × 100 blocs. |
|
||||
|
||||
Le [cahier de cosmologie](cosmologie.md#deux-devenirs-de-la-même-technologie)
|
||||
distingue cette représentation narrative de son futur hébergement technique.
|
||||
Un espace dit « orphelin » devra rester correctement identifié et sauvegardé
|
||||
par Minecraft ; aucune perte réelle de référence ou corruption de chunks n’est
|
||||
prévue. Backrooms et Indoors restent à implémenter.
|
||||
|
||||
La géométrie des indoors reste à décider : certains peuvent être des volumes précisément délimités, d'autres des espaces fermés qui se rebouclent en trois dimensions, de type tore 3D. Leurs objets d'accès sont liés à la mailbox de leur propriétaire et doivent être récupérables s'ils sont perdus. Les règles de propriété, de partage, de transfert et de sortie sûre seront définies avant leur implémentation.
|
||||
|
||||
## Progression personnelle et collective
|
||||
|
||||
### Capacités initiales
|
||||
|
||||
Le joueur commence avec trois cœurs, trois icônes de nourriture, une capacité respiratoire réduite, une vitesse de minage très faible et l'impossibilité de courir. L'armure conserve son fonctionnement habituel.
|
||||
|
||||
L'XP et les niveaux permettent d'acheter des améliorations. L'exemple de courbe souhaitée double les coûts : **1, 2, 4, 8, 16, 32, 64 niveaux**. Les valeurs doivent être testées séparément pour chaque capacité ; il reste notamment à confirmer si chaque achat consomme des niveaux entiers ou une quantité d'XP équivalente.
|
||||
|
||||
Les capacités peuvent atteindre les valeurs usuelles de dix cœurs, dix icônes de nourriture et une respiration complète par la progression normale en XP. Le nombre d'étapes et les plafonds seront arrêtés dans les tickets concernés.
|
||||
|
||||
### Inventaire, prestige et déblocages
|
||||
|
||||
L'inventaire commence à une rangée et peut s'étendre jusqu'à six par la progression normale. La place de la barre rapide dans ce comptage reste à préciser.
|
||||
|
||||
Les **prestiges débloquent des slots de factions**. Ils ne donnent pas de capacités bonus ni de rangées d'inventaire supplémentaires. Le nombre de slots accordés par prestige et leurs règles d'utilisation seront précisés dans les tickets de factions.
|
||||
|
||||
La progression peut débloquer une table de fabrication dans l'inventaire, des capacités de minage et de construction, et l'accès à certaines fonctions de mods communautaires. Les joueurs peuvent également porter des capes et des familiers ; les spawn eggs correspondent à différents pouvoirs de familier.
|
||||
|
||||
Les recettes de Just Enough Items sont révélées par paliers liés aux advancements. Les advancements Minecraft donnent accès aux recettes Minecraft ; ceux de Sanctuary ouvrent les blocs décoratifs, équipements et systèmes correspondants. Il faudra distinguer dans chaque ticket l'affichage des recettes, leur connaissance et l'autorisation réelle de fabriquer.
|
||||
|
||||
Le **Blocodex natif** fournit une mémoire personnelle des blocs : inconnus, vus,
|
||||
minés, déjà possédés, jetés et posés, avec le stock porté actuel séparé de
|
||||
l’historique. Cette base appartient à Sanctuary et reste disponible sans
|
||||
appareil à fabriquer. Les palettes de construction, recherches ou objectifs
|
||||
pourront consulter ces informations côté serveur ; leurs conditions et
|
||||
récompenses seront définies par leurs propres tickets. Le socle alpha.23 et
|
||||
ses limites sont décrits dans [Blocodex](blocodex.md). Ce socle distingue aussi
|
||||
les blocs du terrain chargé des objets en inventaires et stockages, avec des
|
||||
relevés de couverture séparés. Un historique global par jour UTC conserve les
|
||||
actions matérielles observées depuis l’installation. Ces données pourront
|
||||
servir aux palettes, au shop, aux expansions et à la géologie historique des
|
||||
Backrooms ; elles ne définissent pas encore les coûts, transactions ou règles
|
||||
de ballast de ces systèmes.
|
||||
|
||||
### Construction et production
|
||||
|
||||
- **Mining** améliore la vitesse de minage et ouvre le vein mining : extraction de blocs d'une même veine ou sur un plan défini par l'orientation du joueur.
|
||||
- **Building** ouvre des outils de pose et de remplissage de surfaces, dont le vein building.
|
||||
- **Plans et préfabriqués** : conserver et copier ses constructions, puis accéder à un catalogue de prefabs. Une intégration adaptée de Litematica est envisagée. Un ancien concept de « voxelier » dans un ordinateur permettait de créer des modèles en cubes ; cette piste est considérée comme complexe et n'est pas prioritaire.
|
||||
|
||||
## Monnaies, propriétés et échanges
|
||||
|
||||
### Les trois gemmes
|
||||
|
||||
Les trois gemmes forment la palette et la symbolique triangulaire de Sanctuary.
|
||||
|
||||
| Gemme | Fonction souhaitée |
|
||||
| --- | --- |
|
||||
| **Émeraude** | Économie locale des villageois, échanges et travail dans le monde. |
|
||||
| **Rubis** | Monnaie des échanges passant par les services du serveur et ses boutiques. Nouveau minerai à extraire. |
|
||||
| **Saphir** | Réservation et sauvegarde de quantités limitées d'objets dans le catalogue. Nouveau minerai à extraire. |
|
||||
|
||||
### Mailbox, dépôts et boutiques
|
||||
|
||||
Chaque joueur a une **mailbox** et le serveur dispose de **deposit boxes** pour les apports collectifs. Les achats autorisés sont livrés dans la mailbox. Les échanges doivent pouvoir être reliés à la progression du serveur et aux ressources qu'il conserve.
|
||||
|
||||
Le shop propose des offres flash renouvelées toutes les heures. Le joueur peut débloquer jusqu'à neuf emplacements avec son XP. Le black market accueille les offres des joueurs ; le catalogue permet de réserver une quantité limitée d'un objet contre des saphirs. Des extensions et emplacements supplémentaires peuvent apparaître comme récompenses d'événements.
|
||||
|
||||
Le shop pourrait devenir une infrastructure coûteuse à construire, accessible physiquement et située en fin de progression. Sa forme exacte reste ouverte : bâtiment joueur, service du serveur ou combinaison des deux. Des casinos événementiels peuvent être installés dans de grands indoors où les joueurs se retrouvent.
|
||||
|
||||
### Bourse du navet
|
||||
|
||||
Les **navets** s'achètent **uniquement le dimanche, lors de la loterie**. Pendant la semaine, les joueurs peuvent les revendre au shop au cours variable de la **bourse du navet**. Aucun achat de navets n'est proposé les autres jours.
|
||||
|
||||
### Coffre-fort, équipes et braquage
|
||||
|
||||
Chaque joueur peut posséder un seul coffre-fort. Il contient plus de gemmes qu'un inventaire et permet un porte-monnaie utilisable en jeu. Le coffre doit pouvoir être caché et peut être percé avec une **drill en titane**.
|
||||
|
||||
Chaque joueur a une couleur unique, qui sert de couleur d'équipe. Des joueurs peuvent se fédérer en équipes temporaires, factions ou voisinages et partager les gains d'une action, y compris d'un braquage. Les slots de factions se débloquent grâce aux prestiges. Les conditions d'accès, les protections et l'équilibrage entre coopération et conflit devront être explicités dans ces tickets.
|
||||
|
||||
## Villageois et automatisation
|
||||
|
||||
Une bannière placée au-dessus d'une cloche associe un village à une faction. Faire sonner la cloche permet de mettre à jour l'appartenance des villageois concernés à l'équipe associée. Le rayon, le choix de la bannière et les conflits de cloches restent à spécifier.
|
||||
|
||||
Les villageois peuvent être payés en émeraudes pour réaliser des tâches cohérentes avec leur métier vanilla. Trois secteurs se complètent : **récolte**, **transformation** et **services**. Ils peuvent, par exemple, récolter du blé, le déposer dans une boîte ou le moudre. Les copper golems et la redstone participent au transport et aux chaînes de production.
|
||||
|
||||
Les **conveyor belts**, fabriqués notamment avec du cuir de vache, déplacent les objets sous forme de drops dans le sens de pose du bloc. Ils se combinent avec les droppers pour acheminer les ressources sur des distances importantes.
|
||||
|
||||
## Machines, équipements et redstone
|
||||
|
||||
- **Ordinateur 8 bits** : bloc programmable avec six ports d'entrée et de sortie, permettant de construire ses propres comportements de redstone. Des évolutions en contrôleur et en ordinateur d'interface donnent accès, dans le monde, aux systèmes d'expansion.
|
||||
- **Installation d’expansion à définir** : assembleur, bloc contrôleur, bloc terminal, bloc de dépôt et ancre spatiale sont les éléments actuellement envisagés. Leurs rôles et connexions restent à concevoir. L’installation pourra être reconstruite ailleurs que dans la salle ancienne ; voir le [cahier de conception](structures-conception.md#ordinateurs-et-installation-dexpansion).
|
||||
- **Terminal et storage en titane** : stockage de fin de progression, avec jusqu'à 128 coffres connectés consultables par un terminal commun, afin de déposer et retrouver ses objets sans tri manuel constant.
|
||||
- **Chunky et clé de chunk** : bloc fabriquable maintenant un chunk actif. Son activation requiert une clé trouvable dans les donjons et structures, afin que le chargement permanent ait une valeur d'exploration et d'échange. L'orthographe des noms sera fixée à partir de l'historique.
|
||||
- **Particuleur** : émetteur de particules dont l'effet dépend de l'objet inséré et dont l'intensité dépend du signal de redstone.
|
||||
- **Caméra** : photographie le jeu et transforme les images en cartes Minecraft utilisables dans les item frames.
|
||||
- **Œil d'araignée (spider eye)** : révèle les niveaux de lumière dans le monde, pour visualiser l'intensité de l'éclairage.
|
||||
- **Disque blanc** : renommé avec une référence à une vidéo YouTube, il permet d'en jouer le contenu dans un jukebox. La forme exacte de la référence et le comportement audiovisuel restent à décider.
|
||||
- **Équipements en titane d'anomaly** : équipements impossibles à fabriquer, indestructibles, avec des enchantements exceptionnellement forts. Les machines fabriquables en titane forment une catégorie distincte.
|
||||
|
||||
### Armes et explosifs
|
||||
|
||||
L'argent, appelé aussi **silver**, est le métal des armes de précision et de certaines technologies :
|
||||
|
||||
- Le **creeper lock** consomme de la poudre à canon pour une attaque explosive de courte portée.
|
||||
- La **mitraillette** repousse les monstres à moyenne portée. Elle emploie des munitions métalliques de puissance variable ; le choix précis entre pépites et lingots reste à fixer.
|
||||
- Le **mining rifle** est une arme de longue portée utilisant des minerais.
|
||||
|
||||
Ces armes peuvent recevoir des enchantements adaptés. Les nouveaux explosifs comprennent une **Weather TNT** qui dissipe la pluie, une **Fragment TNT** qui libère plusieurs TNT, une **Randomizer TNT** qui redistribue les blocs autour d'elle, et une **Mega TNT** plus puissante.
|
||||
|
||||
Les **lucky blocks** déclenchent volontairement un événement imprévisible, parfois très favorable, parfois franchement nuisible : récompenses rares, mort, poison, sorcières, lave ou dégâts causés par des explosifs. Leur risque et leur injustice font partie du concept ; la zone et les conditions de ces effets seront fixées dans le ticket avant leur ajout à un serveur partagé.
|
||||
|
||||
## Déplacements et interactions
|
||||
|
||||
| Système | Comportement souhaité |
|
||||
| --- | --- |
|
||||
| **Waystones horizontales** | Voyager dans une même dimension vers une waystone connue, avec un coût d'XP croissant selon la distance. |
|
||||
| **Waystones verticales** | Ascenseurs : sauter pour monter, se baisser pour descendre. L'étage de destination n'a pas besoin d'avoir été découvert. |
|
||||
| **Téléporteur longue distance** | Préparation plus lente, mais grandes distances possibles, avec une arrivée approximative. |
|
||||
| **Zipline** | Corde reliant deux installations, jusqu'à une distance indicative de 512 blocs. Un clic droit lance le trajet, moins rapide qu'un minecart à pleine vitesse. |
|
||||
| **Grappling rod** | Viser un point pour s'y tirer, avec un risque réel de chute. Consomme des leads qui ne sont pas récupérés. |
|
||||
| **Bateau avec poule** | Aéronef simple obtenu en plaçant une poule dans un bateau, permettant notamment le transport de villageois. |
|
||||
| **Biplan** | Aéronef à deux places, plus rapide que le bateau volant, mais plus lent que les elytras. |
|
||||
| **Happy Ghast** | Monture volante à quatre places, avec une vitesse doublée. « Happy gust » a été prononcé dans la description ; l'identifiant exact est à vérifier. |
|
||||
| **Totem de Notch / Magic Carpet** | En tenant le totem, faire apparaître une plateforme de verre sous ses pieds pour marcher dans les airs et faciliter la construction. La taille de la plateforme, évoquée comme trois blocs, reste à fixer. |
|
||||
|
||||
Un sneak + clic droit permet de porter un mob sur sa tête, avec une limite d'un mob porté. Sur un autre joueur, le même geste permet de se placer sur sa tête. Un double sneak sans déplacement permet de s'asseoir.
|
||||
|
||||
## Faune, créatures et personnages
|
||||
|
||||
### Hostiles et fantômes
|
||||
|
||||
Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs.
|
||||
|
||||
Le système d'insomnie et les phantoms vanilla disparaissent lorsque le monde passe au temps réel. Ils laissent place à d'autres fantômes, dont une **baleine volante, fantomatique et apaisante**, qui passe autour des joueurs et peut s'inviter dans le paysage comme un photobomb.
|
||||
|
||||
Un **Happy Creeper**, bienveillant, porte une fleur sur la tête et un visage différent. Des creepers rares ont également des apparences variées.
|
||||
|
||||
### Poules rares
|
||||
|
||||
Une poule ordinaire peut très rarement pondre un œuf en or. Les quatre variantes rares sont classées ainsi :
|
||||
|
||||
Les facteurs se cumulent d'une variété à la suivante. Si **T** est l'intervalle moyen entre deux pontes d'une poule ordinaire :
|
||||
|
||||
| Poule | Production | Fréquence de ponte | Intervalle moyen |
|
||||
| --- | --- | --- | --- |
|
||||
| Émeraude | Émeraudes | 2 fois moins souvent qu'une poule ordinaire | 2 × T |
|
||||
| Rubis | Rubis | 4 fois moins souvent que la poule émeraude | 8 × T |
|
||||
| Saphir | Saphirs | 8 fois moins souvent que la poule rubis | 64 × T |
|
||||
| Or | Œufs en or | 16 fois moins souvent que la poule saphir | 1 024 × T |
|
||||
|
||||
Ces quatre variantes **ne peuvent pas se reproduire**. Leur rareté doit préserver la valeur de la monnaie. Les joueurs les protègent avec des armures de poule inspirées des armures de loup, colorables, et peuvent les nommer. L'objectif est de créer de l'attachement et de l'attention à ces animaux.
|
||||
|
||||
### Mooblooms et noms d'entités
|
||||
|
||||
Les **Mooblooms** sont des variantes de vaches très rares associées aux fleurs, avec de petites et grandes versions. La liste initiale comprend hibiscus, narcisse, tulipes roses, orange, rouges et blanches, marguerite/oxeye daisy, tournesol, allium, houstonie/azure bluet, orchidée bleue, bleuet/cornflower, pissenlit, muguet, lilas, rose, rose bleue et coquelicot/poppy. Les libellés exacts, la variante « moobloom » générique et les équivalences linguistiques devront être alignés sur les ressources existantes.
|
||||
|
||||
La progression peut débloquer l'affichage de noms pour toutes les familles d'entités. Chaque famille a un registre humoristique : pseudos « kikoolol Xbox 360 » pour les zombies, alphabet galactique Minecraft pour les Endermen, et des noms variés pour les animaux.
|
||||
|
||||
### Végétation propre à Sanctuary
|
||||
|
||||
Nouveaux bois envisagés : **lavande, ébène, mossy et blueberry**. De nouvelles fleurs comprennent les roses et les roses bleues. Les formes finales des arbres et les noms de registre restent à définir.
|
||||
|
||||
## Temps réel, calendrier, événements et ciel
|
||||
|
||||
L'heure du jeu suit l'heure réelle : à 8 h du matin sur le serveur, il est 8 h dans le monde. L'administrateur choisit le fuseau ou le décalage horaire. Le sommeil ne sert donc plus à faire avancer la nuit, ce qui libère un rôle pour le lit comme entrée dans les Backrooms.
|
||||
|
||||
Un calendrier expose des nombres de jours simples : âge du serveur depuis sa création et nombre de jours écoulés depuis une origine historique de Minecraft. La date exacte de cette origine reste à choisir. Cette chronologie fait partie de la mythologie de Sanctuary.
|
||||
|
||||
Des panneaux d'événements permettent de proposer une activité et de s'inscrire, en lien avec les factions. Des anniversaires issus d'Only Fun peuvent y être reliés. Des événements suivent un cycle évoqué comme six jours actifs et un septième jour de repos ou de fête ; la durée et l'ancrage hebdomadaire doivent être confirmés.
|
||||
|
||||
Une **loterie du dimanche** permet de collecter des tickets pendant la semaine. Le nombre de tickets augmente les chances de récompense : lootboxes anomaly, emplacements du catalogue ou extensions du shop. Les probabilités et le financement des récompenses doivent préserver la rareté des ressources.
|
||||
|
||||
La loterie du dimanche accueille aussi l'unique occasion hebdomadaire d'acheter les navets de la [bourse du navet](#bourse-du-navet), revendables ensuite au shop pendant la semaine.
|
||||
|
||||
### Étoiles et constellations
|
||||
|
||||
Les premières nuits n'ont pas d'étoiles. Les advancements Minecraft et Sanctuary font naître progressivement des familles d'étoiles communes à tout le serveur. Avec une spyglass, le joueur peut observer une étoile, son nom et l'accomplissement qui l'a fait apparaître.
|
||||
|
||||
Il peut tracer des constellations depuis son point de vue, directement dans le ciel, sans ouvrir une interface séparée. Ajouter ou retirer un point coûte un niveau dans l'intention initiale ; il faudra préciser la différence entre point et segment lors de l'implémentation. Les constellations sont visibles par tous. Le ciel tourne plus lentement que le cycle quotidien, de sorte que les nuits évoluent au fil des jours.
|
||||
|
||||
## Quêtes, récompenses et cosmologie
|
||||
|
||||
### Panneaux et lootboxes
|
||||
|
||||
Les panneaux de quêtes utilisent les trois gemmes et leurs couleurs : vert émeraude, rouge rubis, bleu saphir, pour trois paliers de difficulté. Le nombre de quêtes réalisables est limité par heure. Le joueur voit les récompenses avant de choisir ; les quêtes donnent notamment de l'XP et des lootboxes.
|
||||
|
||||
Quatre catégories de lootboxes sont prévues : **normales**, **capes**, **spawn eggs** et **anomaly**. Les anomaly peuvent contenir les équipements en titane impossibles à fabriquer.
|
||||
|
||||
### Le cube originel et les sept boules
|
||||
|
||||
Sanctuary naît d'un **cube originel**, auquel le joueur reste secrètement lié et dont découle sa progression. Une mythologie cachée, inspirée d'une alchimie et d'une cabale minecraftiennes, relie ce cube à sept boules de cristal : six directions ou pointes et un centre. La référence visuelle évoque à la fois une figure à six pointes et les sept Dragon Balls.
|
||||
|
||||
Les objectifs déjà évoqués sont :
|
||||
|
||||
- **Boule de la fortune** : réunir une grande quantité de richesse, à calibrer pour un serveur pensé, par exemple, pour un mois de jeu.
|
||||
- **Boule du cauchemar** : accomplir dans les Backrooms une action consistant à enfermer un mob particulier.
|
||||
- **Boule de Notch** : vaincre Notch dans le monde Alpha ; cette rencontre donne aussi le totem qui active la Magic Carpet.
|
||||
- **Boule de la cité ancienne** : vaincre ou résoudre la rencontre du nécromancien dans les cavernes.
|
||||
|
||||
Les trois autres boules, les noms définitifs et la manière de les réunir restent à concevoir. L'Ender Dragon, Notch, le zombie géant, le premier burger et le premier sushi peuvent tous devenir des événements de l'histoire commune, à des échelles différentes.
|
||||
|
||||
## It's Alive ! — cuisine et agriculture
|
||||
|
||||
It's Alive ! est un mod autonome de cultures, de préparation et de transformation alimentaire. Son intégration à Sanctuary apporte des raisons d'explorer, de commercer et de produire ensemble.
|
||||
|
||||
Les cultures et ingrédients se trouvent dans des biomes appropriés à leur température et à leur humidité. Tout ne pousse pas et ne se trouve pas partout. Certaines variations de récolte suivent une logique comparable à la recherche de baies dans Cobblemon.
|
||||
|
||||
### Contenus envisagés
|
||||
|
||||
| Famille | Exemples issus de la vision initiale |
|
||||
| --- | --- |
|
||||
| Fruits et légumes | Fraise, tomate, ananas, maïs, concombre, poivron, aubergine, laitue, chou, raisin, haricot, piment, olive, citron, oignon, ail |
|
||||
| Céréales, herbes et arômes | Riz, soja, basilic, menthe, coriandre, café, vanille, thé |
|
||||
| Ingrédients transformés | Lait, tofu, beurre, crème, fromage, vinaigre, poudre — nature de cette dernière à préciser |
|
||||
| Fermentations et conserves | Vin, bière, kimchi, cornichons, yaourt, saucisson |
|
||||
| Plats et recettes | Chili, veloutés, soupes, omelettes, sautés, currys, salades, sushis, pain, sandwichs, burgers, gâteaux, préparations fourrées et pizzas |
|
||||
| Outils | Poêle, moulin, marmite, cuisinière, tonneau de macération et séchoir |
|
||||
|
||||
Le vin peut vieillir pour gagner en qualité. Le fromage et le saucisson peuvent également être affinés ; ce vieillissement n'est pas prévu pour la bière dans l'intention actuelle.
|
||||
|
||||
Des **pages culinaires** reconstituent le livre de recettes perdu de Steve. Il ne contient pas toutes les recettes : des sorcières gardent des **secret pages**, obtenues en les combattant, qui révèlent notamment certaines techniques de crème, beurre, vin et bière.
|
||||
|
||||
## Only Fun — interactions sociales et absurdes
|
||||
|
||||
Only Fun rassemble des interactions volontairement potaches : faire pipi, faire caca, vomir, fumer, cultiver du chanvre et célébrer les anniversaires de ses amis. Il peut fonctionner seul et se raccorder au calendrier, aux événements et aux factions de Sanctuary. Son contenu exact doit rester séparé du socle nécessaire pour jouer à Sanctuary.
|
||||
|
||||
## Master Key — administration
|
||||
|
||||
Master Key fournit des permissions et des outils d'administration partagés aux différents mods. L'objectif est qu'un administrateur puisse configurer le serveur, diagnostiquer et réparer une situation sans devoir activer le mode créatif pour jouer. Les actions de maintenance doivent avoir un périmètre clair, être contrôlées par le serveur et laisser des traces exploitables pour comprendre un incident.
|
||||
|
||||
## Décisions à prendre au fil des tickets
|
||||
|
||||
Les inconnues ne bloquent pas l'initialisation ni le terrain de base. Elles sont traitées au moment où elles influencent une fonctionnalité :
|
||||
|
||||
- Taille, altitude, relief, réserve de ressources et identité visuelle de Sanctuary Island.
|
||||
- Forme des continents, distances d'expansion, coûts collectifs et contrôle de leur ouverture.
|
||||
- Adaptation du catalogue TerraMix historique et compatibilité des mods communautaires avec la cible Fabric.
|
||||
- Courbe d'XP, lignes d'inventaire et distinction entre rangée et barre rapide ; conditions de passage des prestiges, nombre et fonctionnement des slots de factions débloqués.
|
||||
- Conditions de perte et de récupération des objets, durée de conservation dans les Backrooms et garantie d'unicité.
|
||||
- Nature et propriété des indoors, partage des accès et comportement à la déconnexion.
|
||||
- Règles des boutiques, du coffre-fort, du braquage et des équipes temporaires.
|
||||
- Identifiants définitifs, noms hérités, ressources réutilisables et licences associées.
|
||||
- Géométrie du ciel partagé et comportement des constellations selon la position du joueur.
|
||||
- Date d'origine du calendrier, rythme des événements et trois boules de cristal encore non décrites.
|
||||
|
||||
La prochaine décision concrète reste la même : obtenir un monde flottant stable et intéressant, puis l'éprouver en jeu avant d'ajouter les systèmes qui le feront grandir.
|
||||
@@ -1,871 +0,0 @@
|
||||
# Premier monde Sanctuary
|
||||
|
||||
## Alpha.13 — génération unifiée
|
||||
|
||||
Le choix public **Sanctuary** (`sanctuary:sanctuary`) crée l’île principale à
|
||||
l’origine de l’Overworld, entourée de vide. Son bouton natif **Personnaliser**
|
||||
propose **Petit** (512 blocs de diamètre nominal), **Moyen** (724, par défaut)
|
||||
et **Grand** (1 024). Les types vanilla restent disponibles. La hauteur reste
|
||||
de 384 blocs ; les dimensions nominales ne donnent ni aire habitable garantie
|
||||
ni capacité multijoueur.
|
||||
|
||||
L’île initiale reprend la densité, les forêts et l’hydrologie complète de la
|
||||
famille Population, avec les poches de biomes souterrains natives du laboratoire.
|
||||
Les extensions conservent leurs profils climatiques et leurs commandes
|
||||
alpha.12.1. Leurs règles de génération restent distinctes de celles de l’île
|
||||
initiale : la grande hydrologie de départ n’est pas automatiquement reproduite
|
||||
sur chaque continent ajouté.
|
||||
|
||||
Les trois tailles et les cavités passent les contrôles moteur. Le [guide alpha.13](alpha13-worldgen.md)
|
||||
décrit l’interface, les paramètres sauvegardés et les outils d’expansion ;
|
||||
[Validation](testing.md) conserve la portée des essais de chaque version.
|
||||
|
||||
## Créer un nouveau monde
|
||||
|
||||
En solo, choisir **Sanctuary**, ouvrir **Personnaliser**, puis confirmer la
|
||||
taille avec **Terminé**. **Annuler** conserve le réglage précédent. Cette
|
||||
interface configure le monde à créer ; elle ne redimensionne pas une partie.
|
||||
|
||||
Sur un nouveau serveur Fabric avec Fabric API, le preset public utilise la
|
||||
taille **Moyen**, 724 blocs :
|
||||
|
||||
```properties
|
||||
level-type=sanctuary:sanctuary
|
||||
level-name=sanctuary-dev
|
||||
level-seed=0
|
||||
```
|
||||
|
||||
La graine est libre. Utiliser un dossier neuf pour un essai ; modifier
|
||||
`level-type` ne convertit aucun monde enregistré. Aucune commande de migration
|
||||
ou de régénération des anciennes îles n’est fournie.
|
||||
|
||||
## Compatibilité et versions historiques
|
||||
|
||||
L’alpha.13 enregistre le générateur `sanctuary:island_v13`, son diamètre initial
|
||||
et une racine de génération 13. Les nouvelles expansions restent de génération
|
||||
12. Leur journal utilise le dossier propre au monde
|
||||
`data/sanctuary-world-v13/`. Les anciens codecs et presets sont conservés, mais
|
||||
cachés de la sélection publique. Les mondes déjà créés conservent leurs réglages
|
||||
et leur journal ; aucun chunk n’est remplacé par cette mise à jour.
|
||||
|
||||
### Alpha.12 et alpha.12.1 — laboratoire historique
|
||||
|
||||
Le preset distinct `sanctuary:expansion_lab` proposait une origine de 512 blocs
|
||||
et des commandes opérateur pour ajouter des îles de 64/128/256/512 blocs.
|
||||
Son relief expérimental et ses biomes natifs ne reprenaient pas les grandes
|
||||
rivières et terrasses de Population. L’alpha.12.1 ajoutait une distance
|
||||
facultative et la recherche d’un emplacement libre au-delà des chunks occupés.
|
||||
|
||||
Ces mondes gardent leur génération 12 et leur dossier
|
||||
`data/sanctuary-expansion-v1/`. Le [guide du laboratoire](alpha12-laboratory.md)
|
||||
reste la référence historique de ces versions, avec leurs seules observations.
|
||||
|
||||
### Alpha.11 — Petit et Grand historiques
|
||||
|
||||
| Ancien choix public | Diamètre nominal | Ancien preset | Réglage enregistré |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit | 512 blocs | `sanctuary:sanctuary_5` | `sanctuary:population_5` |
|
||||
| Grand, ancien défaut | Environ 724 blocs | `sanctuary:sanctuary` | `sanctuary:population_10` |
|
||||
|
||||
L’ancien Grand correspond à la taille nominale du nouveau Moyen. Les mondes
|
||||
historiques `population_5`, `population_10`, `population_20` et `population_100`
|
||||
restent chargeables avec leurs dimensions. Le preset public est une recette
|
||||
de création ; sa nouvelle définition ne remplace pas leur générateur sauvegardé.
|
||||
|
||||
L’optimisation alpha.11 conserve l’arithmétique de l’interpolation et les règles
|
||||
de terrain et d’hydrologie. Ses mesures et contrôles sont historiques, sans
|
||||
validation anticipée des ajouts de l’alpha.13.
|
||||
|
||||
## Socle de génération conservé
|
||||
|
||||
Les points suivants décrivent le socle livré jusqu’à l’alpha.8. Les évolutions
|
||||
alpha.9 et alpha.10 sont précisées dans les sections v9 et v10 ci-dessous.
|
||||
|
||||
- Le générateur reste celui de Minecraft, `minecraft:noise`.
|
||||
- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise`
|
||||
avec `xz_scale=0.25`, `y_scale=0.25`, `xz_factor=80`, `y_factor=160`, `smear=4`,
|
||||
ainsi que son gradient supérieur. Il est échantillonné **64 blocs plus bas**
|
||||
pour relever l'île entière de 64 blocs. Reliefs, surplombs, trous et fragments
|
||||
dépendent de la seed Minecraft. L’alpha.8 y soustrait des failles courbes,
|
||||
sans ajouter de matière. L'ancien gradient inférieur uniforme est
|
||||
remplacé par la sculpture irrégulière décrite ci-dessous.
|
||||
- Le terrain naturel reste libre de laisser un trou à l'origine, de former des
|
||||
surplombs ou de se séparer en fragments. **Aucun noyau ellipsoïdal, cylindre ou
|
||||
plateforme de départ n'est ajouté.** Le relief relevé de 64 blocs reste le
|
||||
champ source ; les failles de l’alpha.8 en retirent de la matière et
|
||||
l’hydrologie traite ensuite les lits et les rives près de la surface.
|
||||
- Le pourtour est sculpté par retrait de matière à partir du rayon **128 blocs**.
|
||||
Le contour de référence de 256 blocs est déformé par les bruits, entre 204 et
|
||||
288 blocs ; il indique la fin de l'érosion, pas un mur de roche.
|
||||
La côte réelle apparaît plus tôt, selon la densité du terrain et la seed.
|
||||
Aucun terrain ne réapparaît au-delà de **288 blocs**, sans répétition d'îles.
|
||||
Les arbres peuvent dépasser la côte rocheuse de quelques blocs.
|
||||
- Les nouveaux mondes utilisent **384 blocs de hauteur**, de `Y=0` à `Y=383`,
|
||||
pour la dimension et le générateur. La roche disparaît avant leurs limites.
|
||||
Il n'y a ni plancher de bedrock,
|
||||
ni mer globale, ni reprise du terrain à distance. Le Nether et l'End restent
|
||||
ceux de Minecraft.
|
||||
- Les nouveaux mondes utilisent une palette tempérée de forêts de chênes et de
|
||||
bouleaux, de clairières fleuries et de zones rocheuses. Les strates intérieures
|
||||
portent des chênes noirs et des champignons sur leurs corniches naturelles.
|
||||
Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent
|
||||
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
|
||||
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
|
||||
- Jusqu’à l’alpha.8, les carvers, géodes, donjons, grands lacs de lave et sources
|
||||
vanilla sont retirés. L’alpha.9 ajoute une intégration bornée de géodes et
|
||||
de geysers, sans réactiver l’ensemble de la décoration souterraine vanilla.
|
||||
L’eau de Sanctuary suit son propre plan de surface et de niches
|
||||
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
|
||||
Aucun mod de biomes externe n’est nécessaire.
|
||||
|
||||
L’alpha.8 associe ces biomes aux plages, aux eaux et aux filons décrits
|
||||
ci-dessous. Les ressources de survie ne se limitent pas au bois et à la pierre,
|
||||
mais cet incrément ne garantit pas encore une partie Minecraft complète sur
|
||||
chaque seed. L’accès à chaque progression vanilla et les ressources renouvelables
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Capacité de l’île — génération v10 historique
|
||||
|
||||
Lors de l’alpha.10, trois tailles étaient accessibles à la création d’un monde.
|
||||
Le choix **Sanctuary** visait 20 joueurs par défaut, accompagné de variantes
|
||||
pour 5 et 100 joueurs. Le tableau suivant conserve cette association historique ;
|
||||
l’alpha.11 utilise le nouveau défaut Grand indiqué plus haut :
|
||||
|
||||
| Preset de création | Paramètres enregistrés dans le monde |
|
||||
| --- | --- |
|
||||
| `sanctuary:sanctuary_5` | `sanctuary:population_5` |
|
||||
| `sanctuary:sanctuary` | `sanctuary:population_20` |
|
||||
| `sanctuary:sanctuary_100` | `sanctuary:population_100` |
|
||||
|
||||
Les nouveaux réglages restent distincts des identifiants enregistrés dans
|
||||
les anciennes sauvegardes. Modifier le preset
|
||||
sélectionné pour une nouvelle partie ne convertit pas un monde existant.
|
||||
|
||||
| Capacité de référence | Facteur horizontal | Aire nominale relative | Diamètre nominal | Hauteur |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 5 joueurs | 1 | 1 | 512 blocs | 384 blocs |
|
||||
| 20 joueurs, défaut | 2 | 4 | 1 024 blocs | 384 blocs |
|
||||
| 100 joueurs | √20, environ 4,472 | 20 | Environ 2 290 blocs | 384 blocs |
|
||||
|
||||
Le facteur horizontal vaut `√(capacité / 5)` : doubler le diamètre multiplie
|
||||
l’aire nominale par quatre. Cette relation décrit l’enveloppe de référence,
|
||||
pas la surface réellement constructible après les trous, les falaises et
|
||||
la sculpture de la bordure. Aucun cylindre central ni remplissage de secours
|
||||
ne doit être ajouté pour obtenir l’aire théorique. La hauteur de dimension
|
||||
reste `Y=0..383`.
|
||||
|
||||
La capacité choisie reste un paramètre de génération. Elle ne modifie pas
|
||||
`max-players`, n’agrandit pas l’île à chaque connexion et ne fixe aucun quota
|
||||
de ressources. Les anciens mondes jusqu’à l’alpha.9 gardent leur génération
|
||||
et leur taille ; leurs chunks ne sont ni remplacés ni régénérés. Le projet
|
||||
de continents déverrouillables reste séparé de ces tailles de départ.
|
||||
|
||||
Le bruit naturel reste échantillonné aux coordonnées du monde ; seule
|
||||
l’enveloppe extérieure change de taille. Les plans hydrauliques sont construits
|
||||
à la demande dans des régions de 512 blocs, avec une graine régionale distincte.
|
||||
Les marges d’admission sont de 16 blocs pour l’eau et 32 pour la lave. Bassins,
|
||||
berges et groupes de terrasses sont admis entiers ; les sources dépendant d’une
|
||||
coupe écartée sont retirées aussi. Aucun trajet n’est tronqué à la frontière.
|
||||
Les couches superposées et leurs sources sont ensuite traduites ensemble.
|
||||
|
||||
Les forêts, minerais et décorations utilisent toute l’emprise du profil.
|
||||
La recherche facultative d’un arbre remarquable reste unique et centrale.
|
||||
Les sources et exutoires n’agissent que pendant la génération, avec les
|
||||
marqueurs vanilla consommés ; recharger un ancien chunk ne rouvre pas une bouche.
|
||||
|
||||
Les trois tailles passent onze tests requis sur la graine `0`, avec 1 800 ticks
|
||||
de fluides et un aller-retour réel des paramètres sur disque. Le fichier
|
||||
`data/minecraft/world_gen_settings.dat` conserve la clé du profil choisi.
|
||||
212 anciens fichiers de production sont strictement identiques, tandis que
|
||||
huit fichiers d’intégration ou d’interface exposent les nouvelles générations.
|
||||
Le premier calcul d’une région reste coûteux ; les résultats ne sont pas un
|
||||
test de cent connexions ni une garantie de génération rapide de toute l’île.
|
||||
Voir les emprises et durées dans [Validation](testing.md).
|
||||
|
||||
## Cavités et terrasses d’eau — génération v9
|
||||
|
||||
Le nouveau réglage est `sanctuary:sanctuary_cavern`, avec le biome source
|
||||
`sanctuary:cavern_island` et la règle de matière `sanctuary:cavern_island`.
|
||||
La densité réutilise `sanctuary:final_density_rift` sans changer cet identifiant
|
||||
historique. Ses nouvelles données et traitements
|
||||
doivent rester distincts de `sanctuary:sanctuary_rift` et de toutes les
|
||||
générations précédentes. Le preset sélectionne la nouvelle génération à la
|
||||
création ; une mise à jour du JAR ne convertit pas une sauvegarde ni ne
|
||||
régénère ses chunks. Les cavités restent dans l’Overworld sur 384 blocs de
|
||||
hauteur ; la dimension des cavernes décrite dans la vision n’est pas ajoutée.
|
||||
|
||||
### Ambiances et ressources
|
||||
|
||||
Le ticket conserve les forêts de surface et ajoute des cavités luxuriantes
|
||||
éclairées par leur végétation, des secteurs à spéléothèmes et des géodes
|
||||
d’améthyste sur des emplacements compatibles. Les sols, plafonds, coquilles
|
||||
et ouvertures doivent être observés après la décoration réelle : une valeur
|
||||
de biome ne prouve ni le décor ni son éclairage. L’objectif est d’utiliser
|
||||
les volumes naturels de l’île, sans remplir ses trous pour accueillir une
|
||||
structure.
|
||||
|
||||
Les dix biomes ont leurs propres identifiants : `cavern_oak_forest`,
|
||||
`cavern_birch_forest`, `cavern_clearing`, `cavern_dry_woodland`,
|
||||
`cavern_rocky_heath`, `cavern_dark_grove`, `cavern_bamboo_grove`,
|
||||
`cavern_sulfur_depths`, `cavern_lush_caves` et `cavern_dripstone_caves`, dans
|
||||
le namespace `sanctuary`. Les deux nouvelles ambiances luxuriantes et à
|
||||
spéléothèmes sont limitées à Y<184 par la palette ; leur décoration recherche
|
||||
des sols entre Y=40 et Y=182, avec trois blocs naturels de support et un
|
||||
plafond situé de 6 à 48 blocs au-dessus.
|
||||
|
||||
Dans les cavités luxuriantes, la mousse remplace le sol compatible et de
|
||||
petites plantes l’accompagnent. Les vraies lianes `cave_vines` et
|
||||
`cave_vines_plant` pendent sur 3 à 8 blocs ; leurs baies émettent de la lumière
|
||||
et restent récoltables. Ce décor n’ajoute pas d’eau. Les spéléothèmes utilisent
|
||||
les blocs `pointed_dripstone` orientés, sur un appui de `dripstone_block`
|
||||
remplaçant de la roche existante, avec une longueur de 1 à 4 blocs selon
|
||||
l’espace disponible.
|
||||
|
||||
La géode utilise une configuration vanilla compacte avec améthyste,
|
||||
améthyste bourgeonnante, calcite et basalte lisse. Son enveloppe doit tenir
|
||||
dans la roche préexistante et un passage de deux blocs de haut la relie à
|
||||
la cavité, en préservant l’améthyste bourgeonnante et les cristaux attachés.
|
||||
Un chunk sur huit est candidat ; au plus une pose réussie est retenue par
|
||||
chunk candidat. Cela ne garantit pas de géode sur une graine.
|
||||
|
||||
Le soufre devient admissible dans une bande climatique plus étroite sous
|
||||
Y=176. La réduction de cette sélection peut être testée indépendamment du
|
||||
terrain ; seule une comparaison des vrais blocs sur un périmètre commun
|
||||
mesure sa diminution dans l’île. Un geyser éventuel creuse un seul bloc de
|
||||
sol pour sa source d’eau, avec `potent_sulfur` dessous et `magma_block` encore
|
||||
en dessous. Le fond, les quatre parois et la hauteur libre sont contrôlés.
|
||||
Le cycle du geyser reste celui de Minecraft ; ce décor est distinct des
|
||||
bassins hydrologiques et son eau doit être comptée séparément.
|
||||
|
||||
Leur présence, leur activité, l’éclairage et la cohabitation avec les eaux
|
||||
et la végétation sont contrôlés dans les mondes de test. Les preuves de ces
|
||||
éléments sont cumulées sur les trois graines. Un site incompatible peut
|
||||
être rejeté ; aucun geyser ni stock n’est garanti pour chaque graine.
|
||||
|
||||
### Lacs et bassins étagés
|
||||
|
||||
Le plan hydrologique conserve la recherche de rivière de
|
||||
l’alpha.8 et vise de plus grands lacs de surface. Les cibles actuelles sont
|
||||
de l’ordre de **700 à 1 100 colonnes** par grand lac, jusqu’à **7 à 8 blocs**
|
||||
de profondeur intérieure et une incision limitée à **12 blocs**. Ce sont des
|
||||
bornes de recherche ; les dimensions réelles sont relevées séparément, sans quota d’eau à
|
||||
remplir. Les fonds, les parois et les dépôts exigent du terrain naturel.
|
||||
|
||||
Les terrasses sont recherchées dans des cavités entre **Y=56 et Y=208**,
|
||||
avec deux paliers par groupe et jusqu’à deux groupes facultatifs.
|
||||
Un palier vise environ **40 à 180 colonnes**, une profondeur de **1 à 3 blocs**
|
||||
et une incision d’au plus **5 blocs**. Le fond doit conserver trois couches
|
||||
de sédiments et deux couches de support naturel ; le plafond et l’espace
|
||||
disponible sont vérifiés. Les mesures moteur précisent les sites effectivement retenus.
|
||||
|
||||
La recherche suit le choix de pente de l’eau vanilla : parmi les passages
|
||||
libres, elle privilégie ceux qui rejoignent la chute la plus proche. Un
|
||||
bassin inférieur doit intercepter ce trajet réel, sans détour creusé pour
|
||||
le rejoindre. Les sorties entre paliers sont déclarées et les cascades sont formées par les
|
||||
ticks de fluides Minecraft. Le premier débordement doit atteindre le second
|
||||
bassin. Celui-ci peut avoir un exutoire terminal qui s’écoule sur les roches
|
||||
ou vers le vide, sans troisième bassin ajouté. Le générateur ne doit ni peindre une colonne
|
||||
d’eau jusqu’au vide ni colmater un trou pour retenir un bassin. Les volumes
|
||||
retenus et les liaisons réelles sont vérifiés séparément après décoration
|
||||
et simulation, y compris aux frontières de chunks. Les cascades peuvent
|
||||
servir à monter ou descendre selon la nage vanilla ; aucune mécanique
|
||||
d’ascenseur supplémentaire n’est prévue. « Rizières » décrit seulement
|
||||
l’aspect du paysage en terrasses, sans ajouter une culture de riz.
|
||||
|
||||
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
|
||||
passent les contrôles après 1 800 ticks, avec preuves cumulées des terrasses
|
||||
connectées, geysers actifs, baies lumineuses, spéléothèmes et améthyste
|
||||
bourgeonnante. Le build complet réussit. Les résultats et leurs limites sont
|
||||
consignés dans [Validation](testing.md). Aucune présence universelle n’est
|
||||
garantie. La [distribution](packwiz.md) est publiée et la même instance Prism
|
||||
est synchronisée, sauvegardes et réglages conservés.
|
||||
|
||||
## Failles, géologie et ressources — génération v8
|
||||
|
||||
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
|
||||
`sanctuary:final_density_rift` et le biome source `sanctuary:rift_island`.
|
||||
Les anciens paramètres `sanctuary`, `sanctuary_384`, `sanctuary_hydrology`,
|
||||
`sanctuary_natural`, `sanctuary_layered` et `sanctuary_woodland` restent séparés.
|
||||
Changer le preset ne migre pas un monde existant ; aucune régénération de
|
||||
chunks ni modification de sauvegarde n’est prévue par cet incrément.
|
||||
|
||||
### Failles et palette
|
||||
|
||||
`RiftShape` définit une à trois traces principales, courbes et ramifiées,
|
||||
de longueur nominale 84 à 160 blocs. Leur largeur varie avec deux bruits en
|
||||
trois dimensions ; le cœur ouvert est de l’ordre de 4 à 12 blocs, avec des
|
||||
parois érodées autour. Ces nombres décrivent les traces avant leur intersection
|
||||
avec la roche, pas une garantie de canyon continu de cette taille. La passe
|
||||
retire seulement de la matière et laisse intactes les zones de rayon inférieur
|
||||
à 20 blocs et extérieur à 224 blocs. Elle ne crée aucun socle central.
|
||||
|
||||
Les huit biomes `rift_oak_forest`, `rift_birch_forest`, `rift_clearing`,
|
||||
`rift_dry_woodland`, `rift_rocky_heath`, `rift_dark_grove`, `rift_bamboo_grove`
|
||||
et `rift_sulfur_depths` ont de nouveaux identifiants dans le namespace
|
||||
`sanctuary`. Les choix climatiques de l’alpha.7 sont conservés en haut. Le
|
||||
soufre devient admissible dans des régions plus larges des étages inférieurs,
|
||||
notamment vers Y=176, selon les bruits de variation et d’humidité. Le test de
|
||||
palette prouve cette possibilité ; seule la lecture des vrais blocs peut
|
||||
prouver une poche accessible sur une graine.
|
||||
|
||||
La végétation vanilla de surface garde ses arbres et ses teintes. La feature
|
||||
`rift_groves` passe ensuite, avant `rift_shore_sugar_cane`, pour choisir les
|
||||
volumes inférieurs encore libres après les arbres. Elle peut placer de gros
|
||||
champignons vanilla, avec des petits champignons dispersés et des sols mêlant
|
||||
mousse, podzol et mycélium. Les supports naturels et la place disponible
|
||||
restent nécessaires ; un arbre remarquable peut échouer si son site est occupé.
|
||||
L’ordre des plantations intérieures donne la priorité aux arbres dans trois
|
||||
cas sur quatre et aux gros champignons dans l’autre cas, selon un choix lié
|
||||
au chunk. Cela laisse des volumes disponibles aux chênes noirs. Dans les
|
||||
biomes `sanctuary:rift_dark_grove` et `sanctuary:rift_bamboo_grove`, l’ardoise
|
||||
des abîmes peut aussi fournir le support naturel des sols et bosquets
|
||||
inférieurs. Son admissibilité est vérifiée au-dessus de chaque pied : seules
|
||||
ces deux variantes humides autorisent ce support.
|
||||
|
||||
### Matières selon la profondeur
|
||||
|
||||
La règle `sanctuary:rift_island` ne change que la matière de blocs déjà solides.
|
||||
Ses nappes utilisent un bruit 3D, avec un bruit distinct pour faire varier les
|
||||
transitions de profondeur.
|
||||
|
||||
| Domaine | Matières |
|
||||
| --- | --- |
|
||||
| Masses supérieures | Pierre, granite, andésite, diorite ; sols enherbés hors affleurements |
|
||||
| Intérieur des bosquets humides | Tuf, cobblestone, cobblestone moussue, boue compactée |
|
||||
| Masses profondes | Ardoise des abîmes, roche noire, basalte |
|
||||
| Poches soufrées | Soufre et cinabre ordinaires sur les masses existantes |
|
||||
|
||||
Les strates humides se mêlent aux matières hautes entre Y=192 et Y=224 dans
|
||||
leurs biomes ; la transition vers les roches profondes s’étend de Y=136 à Y=168.
|
||||
La profondeur sert de critère géologique : ces blocs ne signifient pas qu’une
|
||||
source de lave se trouve forcément à proximité. Aucun bassin, spike, source
|
||||
vanilla ou bloc de soufre puissant du biome Sulfur Caves n’est ajouté.
|
||||
|
||||
Les anciens petits dépôts vanilla de granite, andésite, diorite et tuf sont
|
||||
remplacés par ces nappes dans les seuls biomes `rift_*`. Les anciennes données
|
||||
de biomes et leurs features ne sont pas modifiées.
|
||||
|
||||
### Minerais
|
||||
|
||||
Les features `rift_ore_*` ciblent des tags Sanctuary locaux : les pierres hautes
|
||||
et humides produisent les minerais ordinaires ; ardoise des abîmes, roche noire
|
||||
et basalte produisent leurs variantes d’ardoise. Les tags vanilla ne sont pas
|
||||
élargis globalement. Le soufre et le cinabre restent des matières distinctes.
|
||||
|
||||
| Minerai | Taille du filon configurée | Essais principaux par chunk | Altitudes principales |
|
||||
| --- | ---: | --- | --- |
|
||||
| Charbon | 9 | 8 | Y=96..304 |
|
||||
| Fer | 7 | 4 | Y=88..280 |
|
||||
| Cuivre | 7 | 3 | Y=96..280 |
|
||||
| Or | 5 | 2 | Y=88..224 |
|
||||
| Redstone | 5 | 2 | Y=88..216 |
|
||||
| Lapis | 4 | 1 | Y=104..240 |
|
||||
| Diamant | 4 | Un essai une fois sur deux | Y=80..208 |
|
||||
| Émeraude | 3 | 1 | Y=104..224 |
|
||||
|
||||
Les hauteurs suivent une distribution en trapèze. Charbon, fer et cuivre
|
||||
reçoivent aussi respectivement 4, 2 et 2 essais sur roche supérieure exposée,
|
||||
et autant de recherches de corniche depuis l’air entre Y=64 et Y=288. Un essai
|
||||
peut ne produire aucun bloc. Les tailles configurées ne sont ni des quantités
|
||||
garanties par filon ni des quotas par île. L’exposition à l’air est autorisée
|
||||
pour ces trois minerais et l’émeraude ; une partie des candidats exposés est
|
||||
encore écartée pour les autres minerais. Les stocks devront être relevés après
|
||||
génération, avec leur emprise explicite.
|
||||
|
||||
### Eaux et lave
|
||||
|
||||
`RiftHydrology` cherche des bassins plus grands et une longue liaison calme
|
||||
sur la vraie densité fissurée. Le critère de cet incrément est un chemin d’eau
|
||||
continu d’au moins 140 blocs entre deux bassins au même niveau, alimenté par
|
||||
une source rocheuse et sa cascade entrante. Il ne s’agit pas d’une rivière
|
||||
descendant continuellement en altitude. La recherche parcourt huit directions
|
||||
et pondère les trajets selon le relief et un bruit ; le parcours retenu est
|
||||
lissé sur une échelle de 16 à 32 blocs, puis arrondi avant sa conversion en
|
||||
blocs. Le lit varie entre environ 5 et 7 blocs de largeur, hors élargissement
|
||||
des bassins. Le chemin et ses marges sont à nouveau vérifiés sur la roche
|
||||
après ce lissage : une courbe ne peut pas servir à franchir un trou.
|
||||
|
||||
La roche doit soutenir le fond et les berges. L’incision du lit reste limitée
|
||||
à 16 blocs. Les berges de rivière peuvent retirer jusqu’à 12 blocs pour
|
||||
rejoindre l’eau par paliers progressifs ; les autres rives conservent leur
|
||||
limite de 2 blocs. Les dépôts de 3 à 5 couches ont toujours besoin de supports
|
||||
naturels. Aucun remblai ne franchit une faille ou un trou. Si aucun parcours
|
||||
compatible n’est retenu, le générateur garde les bassins et sources qui
|
||||
conviennent au relief.
|
||||
|
||||
Le calcul initial du plan reste coûteux : les essais finaux sur ce poste
|
||||
mesurent 80,5 secondes pour la graine `0`, 50,7 pour `42` et 59,7 pour
|
||||
`8675309`, hors génération des chunks. Le résultat est mis en cache par état
|
||||
aléatoire du monde ; il n’est pas recalculé pour chaque chunk. L’optimisation
|
||||
de ce premier calcul reste un travail ultérieur.
|
||||
|
||||
La recherche de lave profonde vise une à deux petites sources entre Y=40 et
|
||||
Y=160, en complément de la niche couverte accessible. Cette poche de 18 blocs
|
||||
sources est recherchée d’abord sur une corniche intérieure entre Y=64 et
|
||||
Y=160. Si aucun site profond ne réunit les conditions de support, de voûte
|
||||
et d’accès, la recherche reprend sur les corniches plus hautes, jusqu’à Y=300.
|
||||
Il n’y a pas de poche créée de force lorsque les deux recherches échouent.
|
||||
Les sources supplémentaires laissent Minecraft former leurs coulées par ses
|
||||
ticks, sans colonne préremplie. L’eau, les supports et les marges de végétation doivent être
|
||||
contrôlés après décoration et simulation. La finition des bouches doit rester
|
||||
limitée à la première génération du chunk, sans effacer un aménagement joueur
|
||||
lors d’un rechargement.
|
||||
|
||||
**Validation du 9 septembre 2026 :** le build complet réussit et les contrôles
|
||||
moteur passent sur les trois graines de référence. Les failles, les bassins terminaux,
|
||||
les liaisons de rivière, les champignons et les fluides après 1 800 ticks sont
|
||||
documentés avec les relevés de ressources dans [Validation](testing.md).
|
||||
Ces échantillons ne garantissent pas la présence de chaque ressource sur
|
||||
toutes les graines. Les rapports intermédiaires ne servent pas de preuve
|
||||
pour la version finale ; aucun monde personnel n’est utilisé par ces tests.
|
||||
|
||||
## Sculpture du pourtour — conservée depuis la génération v2
|
||||
|
||||
La version **0.1.0-alpha.2** retire deux contraintes de la génération v1
|
||||
(`0.1.0-alpha.1`) : le noyau central qui remplissait les creux du bruit naturel,
|
||||
et la découpe radiale qui pouvait exposer une tranche verticale à toutes les
|
||||
altitudes. Les identifiants `sanctuary:*` du preset et de la fonction restent
|
||||
stables. Il s'agit d'une évolution du terrain, pas d'une migration de sauvegarde.
|
||||
|
||||
Trois passes composent désormais la densité finale, après l'interpolation du
|
||||
champ historique :
|
||||
|
||||
1. Un bruit horizontal à grande échelle déforme le contour. Dans la bande
|
||||
extérieure, large de 128 blocs avant déformation, une érosion progressive
|
||||
(`2 × t⁴`) augmente le seuil nécessaire pour conserver de la roche. Une
|
||||
érosion supplémentaire, quadratique selon la hauteur et nulle à l'intérieur,
|
||||
fait reculer les faces hautes et basses même lorsque le bruit naturel est
|
||||
saturé dans une grande masse pleine. Elle arrondit le pourtour sans ajouter
|
||||
de volume au centre.
|
||||
2. Un bruit **3D**, échantillonné à la hauteur réelle, déplace ce contour et
|
||||
creuse des renfoncements plus larges. Le bord peut ainsi changer de position
|
||||
avec l'altitude au lieu de prolonger une ligne verticale.
|
||||
3. Un second bruit **3D**, plus fin, ajoute de petites irrégularités. Ces deux
|
||||
passes s'atténuent progressivement vers l'intérieur ; elles ne remplissent
|
||||
jamais un trou du terrain initial.
|
||||
|
||||
Les bruits `island_edge`, `island_sculpt` et `island_detail` sont tous initialisés
|
||||
avec la seed du monde. Ils utilisent respectivement les octaves de base
|
||||
`-7`, `-5` et `-3`. Les nouvelles entrées `sculpt` et `detail` du codec sont
|
||||
optionnelles pour garder les anciennes définitions JSON lisibles ; leur absence
|
||||
ne rétablit pas le noyau de la génération v1.
|
||||
|
||||
L'érosion est toujours soustraite à la densité bornée du terrain. Une densité
|
||||
nulle ou négative ne peut donc jamais devenir solide. Même avec un terrain
|
||||
d'entrée maximalement positif, elle est négative dès le rayon 280, avant la
|
||||
limite de sécurité 288. Deux transitions verticales douces protègent aussi les
|
||||
bornes de hauteur, sans découper de dalle à `Y=0` ou `Y=256`.
|
||||
|
||||
Ces formules sont toujours utilisées par les sauvegardes de la génération v2.
|
||||
La génération v3 conserve la sculpture latérale dans le repère de l'île relevée.
|
||||
|
||||
## Île relevée et dessous irrégulier — génération v3
|
||||
|
||||
La version **0.1.0-alpha.3** crée les nouveaux mondes avec les paramètres et le
|
||||
type de dimension `sanctuary:sanctuary_384` : `min_y=0`, `height=384` et
|
||||
`logical_height=384`. Le nom du preset sélectionné reste `sanctuary:sanctuary`.
|
||||
Le gain de hauteur n'agrandit pas le rayon horizontal et n'active aucun continent.
|
||||
|
||||
Le bruit naturel et les bruits de sculpture sont échantillonnés à `Y−64`.
|
||||
Le profil supérieur et les bords approuvés en alpha.2 sont ainsi relevés sans
|
||||
changer de seed ou étirer l'île. Sur les graines de régression, les sommets
|
||||
proches de `Y=184` se retrouvent vers `Y=248`, avec de l'espace libre au-dessus.
|
||||
|
||||
Sous l'île, déplacer l'ancien gradient inférieur aurait seulement déplacé son
|
||||
plan de coupe. `floating_archipelago_384` conserve donc le bruit naturel sans
|
||||
ce gradient inférieur. Une nouvelle érosion travaille sur une transition de
|
||||
**72 blocs**, dont l'altitude varie avec le bruit 3D `island_underside` et les
|
||||
deux bruits de sculpture existants. Elle laisse les lobes naturels se poursuivre
|
||||
vers le bas, puis les amincit à différentes hauteurs ; elle n'ajoute ni socle ni
|
||||
surface garantie. La transition n'affecte plus le terrain à partir de `Y=144`.
|
||||
Une marge vide protège le bas de la dimension : aucun plancher rocheux ne doit
|
||||
apparaître dans `Y=0..15`. À partir de l’alpha.5, seules les eaux descendantes de
|
||||
sources explicitement déclarées peuvent traverser cette marge.
|
||||
|
||||
Les champs optionnels du codec `vertical_offset`, `height` et `underside` valent
|
||||
respectivement `0`, `256` et un bruit nul par défaut. Seule la nouvelle fonction
|
||||
`sanctuary:final_density_384` active l'offset `64`, la hauteur `384` et le bruit du
|
||||
dessous. Les anciennes fonctions `final_density`, `floating_archipelago` et les
|
||||
paramètres `sanctuary:sanctuary` restent inchangés : les sauvegardes alpha.2 qui
|
||||
les référencent gardent leur hauteur de génération et leur ancien terrain.
|
||||
|
||||
**Créer un nouveau monde pour tester l'île relevée.** L'actualisation du pack ne
|
||||
convertit pas les sauvegardes existantes ; aucun chunk n'est supprimé, déplacé
|
||||
ou régénéré. Il n'existe pas de migration automatique des anciens mondes vers la
|
||||
dimension de 384 blocs. Changer manuellement leurs références de générateur ou
|
||||
de dimension sort de ce contrat de compatibilité.
|
||||
|
||||
## Hydrologie de surface — génération v4
|
||||
|
||||
La version **0.1.0-alpha.4** conserve la densité, la hauteur et le ciel de
|
||||
l’alpha.3. Les nouveaux mondes sélectionnent les paramètres
|
||||
`sanctuary:sanctuary_hydrology` et le biome `sanctuary:starter_forest_hydrology`.
|
||||
Le type de dimension reste `sanctuary:sanctuary_384`. Les anciens paramètres et
|
||||
le biome `sanctuary:starter_forest` restent disponibles et inchangés : ils
|
||||
n’activent pas cette hydrologie dans les sauvegardes antérieures.
|
||||
|
||||
Un plan calculé depuis le relief et la graine recherche des emplacements de
|
||||
surface pour des étangs, des petits lacs et un premier cours d’eau calme.
|
||||
Le lit peut être légèrement creusé dans la roche existante. Avant d’accepter
|
||||
un emplacement, le plan vérifie le fond et les parois en trois dimensions ainsi
|
||||
que l’ouverture vers le ciel. Un trou profond dans la carte de hauteur ne suffit
|
||||
pas à constituer un lac. Aucune digue ni masse de roche n’est ajoutée pour
|
||||
boucher une fuite, et aucun bassin souterrain n’est rempli.
|
||||
|
||||
Les fonds alternent sable, gravier et argile ; les berges basses peuvent recevoir
|
||||
du sable. Dans ce nouveau biome, les disques de sédiments vanilla et les veines
|
||||
de gravier sont remplacés par ces dépôts de rive et de fond : une feature
|
||||
ultérieure ne doit pas transformer leur support en gravier instable. Les
|
||||
sources de magma sous-marines sont également absentes. Après les arbres et la
|
||||
végétation, de la canne à sucre peut pousser sur
|
||||
les rives qui respectent ses règles de placement vanilla. Le nombre de sites,
|
||||
leur eau et leurs ressources dépendent du terrain : le générateur ne complète
|
||||
pas une graine pour atteindre un quota économique.
|
||||
|
||||
Le plan est indépendant des chunks chargés. Chaque chunk applique uniquement
|
||||
ses propres colonnes après le terrain et avant la décoration, ce qui permet de
|
||||
raccorder un même plan d’eau aux frontières des chunks. Les anciens chunks ne
|
||||
sont jamais retouchés, et aucun travail hydrologique ne se déclenche au simple
|
||||
chargement d’une sauvegarde.
|
||||
|
||||
Cette première version vise de petites eaux retenues et des cours d’eau calmes
|
||||
sur les plateaux. Le cours d’eau est un chenal à niveau constant reliant des
|
||||
plans d’eau ; il ne simule pas encore une pente vers l’aval ou un débit. Les
|
||||
tracés peuvent passer par les diagonales, mais leur eau reste connectée de
|
||||
bloc en bloc. L’excavation est limitée à six blocs, l’eau à quatre blocs de
|
||||
profondeur, avec un fond plein sur trois blocs d’épaisseur, dont deux blocs naturels sous
|
||||
le sédiment. Ce contrat alpha.4 reste celui de ses sauvegardes. Les sources en cascade sont
|
||||
ajoutées séparément dans la génération v5 ; les grandes rivières en pente, océans,
|
||||
aquifères, climats multiples et continents restent des incréments distincts.
|
||||
|
||||
## Plages, sources et mosaïque tempérée — génération v5
|
||||
|
||||
L’implémentation **0.1.0-alpha.5** sélectionne la nouvelle clé de paramètres
|
||||
`sanctuary:sanctuary_natural` (`SanctuarySpawn.NATURAL_SETTINGS`). Elle conserve
|
||||
la densité relevée, les 384 blocs de hauteur et le ciel de l’alpha.3. La classe
|
||||
`NaturalHydrology` et son application sont séparées de `SurfaceHydrology` et
|
||||
`HydrologyRuntime` : le code, les paramètres `sanctuary:sanctuary_hydrology` et
|
||||
le biome `sanctuary:starter_forest_hydrology` de l’alpha.4 restent inchangés.
|
||||
Les clés alpha.2 et alpha.3 restent également disponibles avec leur comportement
|
||||
antérieur. **Ces changements concernent uniquement les nouveaux mondes** : aucune
|
||||
conversion de sauvegarde, régénération de chunk ou expansion n’est effectuée.
|
||||
|
||||
### Dépôts et eau de surface
|
||||
|
||||
Le plan recherche des étangs, de petits lacs et un cours d’eau calme dans le
|
||||
relief naturel. L’excavation maximale passe à **quatre blocs**, pour une eau de
|
||||
**trois blocs de profondeur** au plus. Les bassins gardent un fond et des parois
|
||||
naturels pleins ; aucune digue n’est ajoutée et aucune chambre souterraine n’est
|
||||
remplie. Le cours d’eau reste à niveau constant : il ne modélise pas un débit
|
||||
ni une pente continue vers l’aval.
|
||||
|
||||
Les plages sont des nappes irrégulières qui suivent les faibles variations du
|
||||
relief autour des plans d’eau. Elles peuvent s’étendre sur plusieurs blocs,
|
||||
se rapprocher d’une autre poche d’eau et retrouver progressivement l’herbe ou
|
||||
la pierre. Les matériaux varient entre sable, gravier, pierre et terre enherbée ;
|
||||
l’argile peut apparaître dans les fonds. Le traitement d’une terrasse sèche
|
||||
retire au plus **deux blocs** du relief existant.
|
||||
|
||||
Chaque dépôt remplace **trois à cinq couches** de terrain déjà plein et garde
|
||||
**deux couches naturelles de support** en dessous. Ce support permet notamment
|
||||
au sable et au gravier de rester en place après les mises à jour de gravité.
|
||||
Les veines de gravier, disques de sédiments et magma sous-marin vanilla ne sont
|
||||
pas réintroduits dans ces nouveaux biomes. Les autres décorations rocheuses et
|
||||
les minerais peuvent encore remplacer la pierre ; ils ne remplacent pas les
|
||||
couches de sable, gravier ou argile. Les rives compatibles peuvent porter de la
|
||||
canne à sucre après la végétation. Les troncs couchés vanilla peuvent occuper
|
||||
quelques blocs de la surface d’un étang ; les contrôles distinguent cette
|
||||
décoration de l’eau restante et vérifient leur fond ou l’eau sous les troncs,
|
||||
les sources voisines et la rétention du bassin.
|
||||
|
||||
### Sources rocheuses et cascades
|
||||
|
||||
Le plan peut retenir jusqu’à **trois sources** dans des niches rocheuses
|
||||
naturelles, séparées des dépôts modifiés. Une source remplace un seul bloc de
|
||||
roche, sous un toit, sur un fond et entre trois côtés déjà pleins. Son unique
|
||||
sortie horizontale est naturellement ouverte sur une chute d’au moins huit
|
||||
blocs. Le générateur ne construit ni pylône, ni corniche, ni tunnel pour obtenir
|
||||
ce résultat.
|
||||
|
||||
Seul le bloc source est généré. Les ticks de fluide de Minecraft forment la
|
||||
cascade et ses éventuels passages sur des paliers. Ces écoulements peuvent
|
||||
descendre **jusque dans le vide**, conformément au comportement demandé. Cette
|
||||
exception concerne les sources déclarées. Le trajet calculé dans la roche nue
|
||||
est une prévision : les arbres générés ensuite peuvent dévier une chute. Les
|
||||
diagnostics vérifient donc les connexions réelles de chaque bloc d’eau vers une
|
||||
source, dans l’emprise des chunks inspectés et de leur halo. Ils échouent si de
|
||||
l’eau touche une frontière non inspectée ou si un bassin perd sa coque. Les
|
||||
déviations par rapport à la prévision restent mesurées dans le rapport. Le
|
||||
comportement des fluides reste celui de Minecraft, y compris lorsque les joueurs
|
||||
modifient une rive.
|
||||
|
||||
### Petite réserve de lave
|
||||
|
||||
`StarterLavaDeposit` recherche au plus une niche de **18 blocs sources** dans
|
||||
la roche existante, accessible par un passage sec de deux blocs de haut depuis
|
||||
une corniche naturelle. Le fond garde trois blocs pleins, les parois deux et
|
||||
la couverture huit ; ces supports sont vérifiés avant les écritures. La niche
|
||||
reste éloignée des eaux et de leurs parcours. Elle fournit de la lave prélevable
|
||||
pour les premières productions et un portail du Nether. Si aucun site ne
|
||||
convient, le plan reste vide : aucun bassin artificiel n’est ajouté dans le vide.
|
||||
|
||||
Les tests vérifient l’accès praticable, la rétention de la lave et l’absence
|
||||
d’incendie après simulation.
|
||||
|
||||
### Biomes et minerais
|
||||
|
||||
Le biome source `sanctuary:temperate_island` utilise deux champs de bruit
|
||||
initialisés par la seed pour répartir cinq variantes horizontales. Une rive et
|
||||
la roche sous elle restent dans la même variante :
|
||||
|
||||
| Identifiant | Variante |
|
||||
| --- | --- |
|
||||
| `sanctuary:temperate_forest` | Forêt dominante |
|
||||
| `sanctuary:temperate_plains` | Clairières et plaines tempérées |
|
||||
| `sanctuary:temperate_birch_forest` | Forêt de bouleaux |
|
||||
| `sanctuary:temperate_flower_forest` | Forêt fleurie |
|
||||
| `sanctuary:temperate_dappled_forest` | Dappled Forest, peupliers et végétation associée |
|
||||
|
||||
Dappled Forest est un biome vanilla introduit dans
|
||||
[Minecraft 26.3 Snapshot 1](https://www.minecraft.net/en-us/article/minecraft-26-3-snapshot-1).
|
||||
Cette variante reprend ses données de **26.3-pre-2**, avec une humidité légèrement
|
||||
relevée pour Sanctuary. Elle n’ajoute ni dépendance externe ni structure de camp.
|
||||
La palette tempérée ne constitue pas encore le catalogue TerraMix ni le champ
|
||||
climatique des futurs continents.
|
||||
|
||||
Les nouveaux biomes remplacent les filons vanilla de métaux par de petites
|
||||
features `sanctuary:island_ore_*`, à des altitudes adaptées à l’île relevée :
|
||||
|
||||
| Ressource | Taille du filon | Tentatives par chunk | Plage Y |
|
||||
| --- | --- | --- | --- |
|
||||
| Charbon | 6 | 2 | 96–288 |
|
||||
| Fer | 4 | 2 | 88–248 |
|
||||
| Cuivre | 4 | 1 | 96–248 |
|
||||
| Or | 4 | 1 | 104–224 |
|
||||
| Redstone | 4 | 1 | 96–224 |
|
||||
| Lapis | 3 | Une tentative, chance 1/2 | 112–232 |
|
||||
| Diamant | 3 | Une tentative, chance 1/4 | 104–208 |
|
||||
|
||||
Ces paramètres décrivent des tentatives, pas des quantités promises : le filon
|
||||
doit rencontrer la roche existante, et sa forme dépend du hasard de génération.
|
||||
La sélection des biomes et des eaux ne cherche aucun stock cible. Un relevé
|
||||
partiel reste un relevé partiel ; la présence des sept ressources sur une seed
|
||||
de référence ne garantit pas leur présence sur toutes les seeds.
|
||||
|
||||
La validation de l’alpha.5 est suivie dans [Validation](testing.md). Ses diagnostics
|
||||
sont conçus pour inspecter les blocs décorés, les profils des dépôts, les
|
||||
jointures de chunks, les biomes et les ressources, puis simuler réellement les
|
||||
fluides dans les chunks des sites. Les cartes et profils PNG montrent des
|
||||
lectures de blocs, pas un rendu Minecraft. La descente des
|
||||
cascades est contrôlée après activation effective des chunks qui les simulent.
|
||||
|
||||
## Prairies et corniches — génération v6
|
||||
|
||||
L’alpha.6 utilise `sanctuary:sanctuary_layered` et le biome source
|
||||
`sanctuary:layered_island`. Les formes et le ciel restent ceux de la génération
|
||||
relevée. Les anciennes classes, biomes et règles de matériaux restent disponibles
|
||||
pour les sauvegardes alpha.2 à alpha.5 ; seul le preset de création choisit v6.
|
||||
|
||||
### Une palette qui varie aussi en hauteur
|
||||
|
||||
Les cinq variantes partagent leurs teintes d’herbe et de feuillage. Les champs
|
||||
de climat liés à la seed déforment de larges bandes d’altitude, autour de Y154
|
||||
et Y214 : prairies et prairies fleuries sur le dessus, bosquets et corniches
|
||||
moussues plus bas. Des zones rocheuses traversent ces bandes et remplacent la
|
||||
couverture de terre par de la pierre sur le relief existant. Les prairies gardent
|
||||
quelques arbres épars et leur végétation vanilla. Dappled Forest n’appartient
|
||||
plus à cette palette ; son utilisation sur de futurs continents reste à développer.
|
||||
|
||||
| Identifiant | Ambiance |
|
||||
| --- | --- |
|
||||
| `sanctuary:layered_meadow` | Prairie tempérée |
|
||||
| `sanctuary:layered_flower_meadow` | Prairie fleurie |
|
||||
| `sanctuary:layered_lower_grove` | Bosquet inférieur |
|
||||
| `sanctuary:layered_mossy_terraces` | Corniches moussues |
|
||||
| `sanctuary:layered_rocky_terraces` | Affleurements rocheux |
|
||||
|
||||
`LowerTerraceVegetationFeature` parcourt aussi les sols naturels sous les
|
||||
surplombs, au moins huit blocs sous une surface supérieure. Elle demande trois
|
||||
blocs pleins de support et deux blocs libres au-dessus. Elle peut remplacer la
|
||||
surface existante par de la mousse et planter fougères, fleurs ou petits arbustes
|
||||
lorsque leurs règles de survie le permettent. Les cellules hydrologiques, les
|
||||
sources, les parcours d’eau prévus et les supports de lave sont protégés.
|
||||
Il n’y a ni corniche construite dans l’air ni ajout d’éclairage artificiel.
|
||||
|
||||
### Bassins et sources de paroi
|
||||
|
||||
`LayeredHydrology` conserve les petits bassins et les dépôts épais, avec une
|
||||
plus grande présence de pierre. Le court `STREAM` à niveau constant de l’alpha.5
|
||||
n’est pas repris. Cette version retient uniquement les étangs et les petits
|
||||
lacs ; elle ne cherche pas de grande rivière. Le parcours traversant avec
|
||||
source, cascade et bassin reste un ticket distinct, à démontrer sur le relief
|
||||
Minecraft réel. Aucun chenal de secours n’est creusé.
|
||||
|
||||
La profondeur reste limitée à trois blocs et l’incision à quatre. Le plan
|
||||
vérifie le fond et les rives avant d’appliquer ses écritures, dans les nouveaux
|
||||
chunks seulement. Le sable, le gravier, l’argile et la roche suivent les rives.
|
||||
Des niches de source sont également recherchées dans les strates inférieures,
|
||||
et pas seulement près de la surface la plus haute. Leurs chutes sont produites
|
||||
par les ticks de Minecraft à partir d’un seul bloc source. Le post-traitement
|
||||
des nouveaux chunks libère uniquement leur ouverture, déjà vide dans la densité
|
||||
naturelle, pour qu’un lichen ou une plante ne bouche pas la source. Les positions
|
||||
sont marquées dans la file vanilla pendant la génération, puis consommées une
|
||||
seule fois. Un rechargement ne dégage pas une bouche modifiée par un joueur.
|
||||
Aucune chute n’est préremplie et aucun nouveau passage n’est creusé dans la roche.
|
||||
|
||||
Les parcours, les écoulements et la végétation réellement observés, ainsi que
|
||||
les limites du modèle, sont consignés dans [Validation](testing.md).
|
||||
|
||||
## Apparition commune
|
||||
|
||||
Une injection limitée à la création initiale cherche du sol naturel depuis
|
||||
l'origine vers l'extérieur, dans toute l'emprise finie de l'île. Elle préfère
|
||||
une surface stable de 3 × 3 blocs, avec deux blocs libres en hauteur. En
|
||||
l'absence d'une telle surface, elle retient une colonne de sol praticable ; si
|
||||
aucun emplacement naturel sûr n'existe, la création signale la seed concernée
|
||||
plutôt que d'ajouter une plateforme. Elle ne s'applique qu'à l'Overworld
|
||||
utilisant les paramètres `sanctuary:sanctuary`, `sanctuary:sanctuary_384` ou
|
||||
`sanctuary:sanctuary_hydrology`, `sanctuary:sanctuary_natural` ou
|
||||
`sanctuary:sanctuary_layered`.
|
||||
Elle enregistre le spawn partagé dans la sauvegarde et ne le réinitialise pas
|
||||
au redémarrage : les modifications administratives de `/setworldspawn` restent
|
||||
conservées. Les lits et la dispersion habituelle du spawn suivent encore les
|
||||
règles de Minecraft.
|
||||
|
||||
## Adaptation 26.2 → 26.3
|
||||
|
||||
Les références historiques sont
|
||||
`sanctuary/worldgen/density_function/base_3d_noise.json` et
|
||||
`floating_archipelago.json` dans l'ancien module Sanctuary. L'ancien masque
|
||||
annulaire n'est pas conservé : après son anneau de vide, il faisait réapparaître
|
||||
un archipel infini. Les anciennes surcharges de `minecraft:normal`, dimensions
|
||||
supplémentaires, intégrations TerraMix et injections globales de features ne
|
||||
font pas partie de ce port.
|
||||
|
||||
Minecraft 26.3 a changé l'API de génération : les fonctions de densité compilent
|
||||
maintenant un `DensitySampler`, leurs opérateurs JSON utilisent
|
||||
`left` / `right` / `input`, les gradients utilisent `from_coordinate` et les
|
||||
surfaces passent par le registre `worldgen/material_rule`. Les données de cette
|
||||
branche suivent ces formats réels ; copier directement les JSON 26.2 ne suffit
|
||||
pas. Les bruits utilisent également `base_octave`, `octave_count` et
|
||||
`amplitude_modifiers`. Le seul opérateur Java ajouté, `sanctuary:main_island`,
|
||||
compose le terrain et les bruits de sculpture, applique le repère vertical
|
||||
choisi et impose la limite extérieure après interpolation.
|
||||
|
||||
## Vérification
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:worldgenSmoke
|
||||
./gradlew :sanctuary:runGameTest
|
||||
```
|
||||
|
||||
Le smoke test vérifie qu'aucun sol n'est ajouté, que l'intérieur conserve le champ
|
||||
naturel et que l'érosion évolue progressivement sur un profil de terrain contrôlé.
|
||||
Il vérifie aussi qu'un lobe plein recule d'au moins 30 blocs en haut et en bas
|
||||
par rapport à son milieu, pour éviter une paroi verticale simplement déplacée.
|
||||
Il couvre aussi l'influence des deux bruits de sculpture, le vide avant les
|
||||
bornes de sécurité, et le vide dans les quatre directions même avec un bruit
|
||||
entièrement positif. Les coordonnées testées incluent les limites de Minecraft
|
||||
et des entiers Java pour détecter débordements et répétitions involontaires.
|
||||
|
||||
Les GameTests chargent le preset réel sur un serveur éphémère, vérifient le
|
||||
spawn après décoration, puis inspectent des chunks complets à distance dans
|
||||
les quatre directions. Ils comparent aussi la densité compilée avec deux
|
||||
instances de la même seed et une seed différente. Cela détecte les références
|
||||
JSON invalides et les blocs que des features pourraient ajouter après le calcul
|
||||
de densité. Les injections du serveur de test sont isolées dans
|
||||
`src/gametest` et ne sont pas distribuées dans le mod.
|
||||
|
||||
Les graines de régression des générations v2 et v3 sont **0**, **42** et **8675309**.
|
||||
Le serveur de test exporte leurs cartes de hauteur et coupes de densité dans
|
||||
`mods/sanctuary/build/run/gameTest/diagnostics/`. Ces images permettent de
|
||||
comparer la vraie densité alpha.2, toujours chargée, avec la nouvelle île relevée
|
||||
dans toute la hauteur `0..383`. Les assertions vérifient la translation du relief
|
||||
supérieur, la variation des hauteurs du dessous, l'absence de plancher, les
|
||||
384 blocs de génération et la conservation des anciens paramètres 256.
|
||||
Ces images ne montrent
|
||||
pas les arbres ni les matériaux et ne remplacent pas un essai visuel en jeu.
|
||||
|
||||
## Contrat du prochain ticket : expansion
|
||||
|
||||
Avant de créer des continents, définir une donnée **persistante par monde**
|
||||
contenant pour chaque région son identifiant, centre, direction, emprise,
|
||||
seed dérivée, version de générateur et état de déblocage. L'île principale
|
||||
devient la première région de ce registre ; les nouvelles régions utilisent
|
||||
des coordonnées absolues, sans cellules périodiques.
|
||||
|
||||
L'activation doit être une action du serveur, reproductible et sauvegardée,
|
||||
conditionnée ensuite par les contributions collectives. Elle doit tenir compte
|
||||
du fait qu'un joueur peut déjà avoir chargé ou construit dans le vide : un
|
||||
chunk existant ne se régénère pas simplement parce que la liste des continents
|
||||
change. Le ticket doit choisir une réservation d'espace ou une matérialisation
|
||||
contrôlée qui protège les constructions et ne remplace jamais aveuglément des
|
||||
chunks sauvegardés.
|
||||
|
||||
La version de génération et les emprises doivent être gelées avant les premières
|
||||
sauvegardes destinées à durer. Ce prototype ne promet pas encore de migration
|
||||
automatique des mondes entre deux versions de terrain.
|
||||
|
||||
|
||||
## Forêts, corniches humides et affleurements — génération v7
|
||||
|
||||
L’alpha.7 crée les nouveaux mondes avec `sanctuary:sanctuary_woodland` et
|
||||
`sanctuary:woodland_island`. La densité `sanctuary:final_density_384`, les
|
||||
formes rocheuses, la hauteur et le ciel restent identiques. Les plans des
|
||||
bassins, sources et petites niches de lave reprennent les algorithmes alpha.6.
|
||||
Les anciens paramètres, biomes et features sont conservés ; cette livraison
|
||||
ne modifie pas les mondes existants.
|
||||
|
||||
La palette comporte huit biomes :
|
||||
|
||||
| Identifiant | Rôle |
|
||||
| --- | --- |
|
||||
| `sanctuary:woodland_oak_forest` | Forêt de chênes dominante, bouleaux secondaires |
|
||||
| `sanctuary:woodland_birch_forest` | Forêt de bouleaux complémentaire |
|
||||
| `sanctuary:woodland_clearing` | Clairière fleurie |
|
||||
| `sanctuary:woodland_dry_woodland` | Bois sec plus clairsemé |
|
||||
| `sanctuary:woodland_rocky_heath` | Masses rocheuses exposées |
|
||||
| `sanctuary:woodland_dark_grove` | Corniches humides à chênes noirs et champignons |
|
||||
| `sanctuary:woodland_bamboo_grove` | Rare intérieur à jungle et bambou |
|
||||
| `sanctuary:woodland_sulfur_depths` | Rare poche profonde de soufre sur roche existante |
|
||||
|
||||
Une altitude élevée ne transforme plus automatiquement une forêt en prairie
|
||||
ou en roche. Les forêts utilisent les véritables arbres vanilla avec leurs
|
||||
conditions de croissance ; les affleurements sont des patches horizontaux
|
||||
et des variations de matériau sur le terrain existant.
|
||||
|
||||
`woodland_groves` place de vrais chênes noirs sur des sols naturels de 2×2,
|
||||
avec trois blocs de support et une place suffisante pour le tronc et la
|
||||
canopée sous le plafond. Des champignons accompagnent les sols de podzol et
|
||||
de mycélium, avec quelques touches de mousse. La jungle et le bambou restent
|
||||
limités aux rares poches qui leur conviennent.
|
||||
|
||||
Un seul emplacement remarquable est recherché par graine, avec une essence
|
||||
choisie parmi cerisier (favorisé), épicéa, jungle, acacia, palétuvier et chêne
|
||||
pâle. Il ne produit pas de terrain : si le site ou les conditions de l’arbre
|
||||
vanilla ne conviennent pas, il ne pousse pas. Ce n’est pas une garantie de
|
||||
chaque essence sur chaque île. Aucun cœur de Creaking n’est ajouté par l’arbre
|
||||
pâle utilisé ici.
|
||||
|
||||
Les affleurements mêlent des nappes cohérentes de pierre, andésite, diorite et
|
||||
granite. Les berges conservent leur excavation limitée et leurs dépôts de
|
||||
3 à 5 blocs, avec des patches de sable, gravier, argile et roche. Les plantations de `woodland_groves` évitent les sources, leurs exutoires
|
||||
et les niches de lave. Les décorations vanilla conservent leurs placements ;
|
||||
le traitement unique après génération peut rouvrir la seule bouche de source
|
||||
déclarée si une plante l’obstrue. Le soufre
|
||||
reste une matière rare des profondeurs ; les grands bassins et sources
|
||||
vanilla du biome Sulfur Caves ne sont pas activés.
|
||||
|
||||
Les anciens petits filons souterrains de charbon, fer et cuivre étaient peu
|
||||
susceptibles d’atteindre les surfaces hautes ; leur réglage écartait aussi une
|
||||
partie des blocs voisins de l’air. Les nouvelles features `woodland_ore_*`
|
||||
utilisent des tailles vanilla limitées (`size=6` pour le charbon, `size=5`
|
||||
pour fer/cuivre), sans garantir ce nombre de blocs produits par filon,
|
||||
autorisent cette exposition et ajoutent des essais aléatoires sur les roches
|
||||
de surface et les corniches rencontrées depuis l’air. Elles remplacent
|
||||
seulement les roches naturelles reconnues par Minecraft, dont andésite,
|
||||
diorite et granite. Les autres minerais et les anciens générateurs gardent
|
||||
leurs réglages ; aucune quantité cible n’est réinjectée après mesure.
|
||||
|
||||
La version et les graines de validation sont consignées dans [Validation](testing.md).
|
||||
@@ -1,13 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Verified 2026-09-08. 26.3 is still a pre-release; do not silently move worlds.
|
||||
minecraft_version=26.3-pre-2
|
||||
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.4
|
||||
pack_version=0.1.0-alpha.30.4
|
||||
maven_group=fr.koka.sanctuary
|
||||
Vendored
BIN
Binary file not shown.
-8
@@ -1,8 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -1,253 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then
|
||||
JAVA_HOME=$APP_HOME/.jdk/current
|
||||
export JAVA_HOME
|
||||
fi
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
-93
@@ -1,93 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
[
|
||||
{
|
||||
"version": "beta.001",
|
||||
"tag_name": "notes/beta.001",
|
||||
"name": "Sanctuary beta.001 — Les quatre anciennes expéditions",
|
||||
"body_file": "notes/beta.001.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "d2faf130a76135e466a160376263915a711c20703dbdba4c1261a4d01b204adf"
|
||||
},
|
||||
{
|
||||
"version": "beta.002",
|
||||
"tag_name": "notes/beta.002",
|
||||
"name": "Sanctuary beta.002 — Biomes boréaux, corail et temples",
|
||||
"body_file": "notes/beta.002.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "517197580174ddd46d1c6b5be20829e91ed2daa61c635b8b369ea9afad3ab4d2"
|
||||
},
|
||||
{
|
||||
"version": "beta.003",
|
||||
"tag_name": "notes/beta.003",
|
||||
"name": "Sanctuary beta.003 — Créer son habitant et commencer la progression",
|
||||
"body_file": "notes/beta.003.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "8154048c7b2afff8a3c42e7993cc899de10716b4106fa00bc3f6f7d2827b0963"
|
||||
},
|
||||
{
|
||||
"version": "beta.004",
|
||||
"tag_name": "notes/beta.004",
|
||||
"name": "Sanctuary beta.004 — Cadre Blocodex et sprint",
|
||||
"body_file": "notes/beta.004.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "7e587dc5519573752dd955fb4c34d9f5fae824f867b4dcbec514e5bab9d6768f"
|
||||
},
|
||||
{
|
||||
"version": "beta.005",
|
||||
"tag_name": "notes/beta.005",
|
||||
"name": "Sanctuary beta.005 — Carte native et noms de couleurs",
|
||||
"body_file": "notes/beta.005.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "eafb297b4b2e23ab51244c817114d1c1fe55edfbea5a7eb41dadaec1b1f0a4d1"
|
||||
},
|
||||
{
|
||||
"version": "beta.006",
|
||||
"tag_name": "notes/beta.006",
|
||||
"name": "Sanctuary beta.006 — Brouillard, grille et Demeure",
|
||||
"body_file": "notes/beta.006.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "af67ab386fae0e2ed2e0833e206cb6a4681bbbeee5cb3f77e04ec31eb2f76023"
|
||||
},
|
||||
{
|
||||
"version": "beta.007",
|
||||
"tag_name": "notes/beta.007",
|
||||
"name": "Sanctuary beta.007 — Carte immersive",
|
||||
"body_file": "notes/beta.007.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6ef0c1df5b317159a0dd05b9e5d4535d60cf77c7e700cc82cbc4bc63f1a673bc"
|
||||
},
|
||||
{
|
||||
"version": "beta.008",
|
||||
"tag_name": "notes/beta.008",
|
||||
"name": "Sanctuary beta.008 — Minage et vein mining",
|
||||
"body_file": "notes/beta.008.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "5873b41ee191e709ad1b12fbbbea3b237c974e29740006923df3f295ece5ddd7"
|
||||
},
|
||||
{
|
||||
"version": "beta.009",
|
||||
"tag_name": "notes/beta.009",
|
||||
"name": "Sanctuary beta.009 — Construction et vein building",
|
||||
"body_file": "notes/beta.009.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "1e6b325a3c5f337b711a7c6506ea8adc665c4f0ac687f9f0ef221ff8e99fe17d"
|
||||
},
|
||||
{
|
||||
"version": "beta.010",
|
||||
"tag_name": "notes/beta.010",
|
||||
"name": "Sanctuary beta.010 — Inventaire jusqu'à six rangées",
|
||||
"body_file": "notes/beta.010.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6bb2e7a0b6428ce0481cf565123aca6b3781e00a199dad7001302c3006a126eb"
|
||||
},
|
||||
{
|
||||
"version": "beta.011",
|
||||
"tag_name": "notes/beta.011",
|
||||
"name": "Sanctuary beta.011 — Factions, prestige et New Game+",
|
||||
"body_file": "notes/beta.011.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "40b82f673945db70e2654181eba8cc022b2d1dec6375bf6b0d659ebfbfa2762b"
|
||||
},
|
||||
{
|
||||
"version": "beta.012",
|
||||
"tag_name": "notes/beta.012",
|
||||
"name": "Sanctuary beta.012 — Inventaire et progression lisible",
|
||||
"body_file": "notes/beta.012.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "17eed6551ae82eef2634b1f9a7a59c19a74a542c66847aca56183e60179596cf"
|
||||
},
|
||||
{
|
||||
"version": "beta.013",
|
||||
"tag_name": "notes/beta.013",
|
||||
"name": "Sanctuary beta.013 — Progression, prestige et repères visuels",
|
||||
"body_file": "notes/beta.013.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "92f9e99124d10c33f22e2d376d988ef9f2e2290d62c6cb32794b45583c3e4ab4"
|
||||
},
|
||||
{
|
||||
"version": "beta.014",
|
||||
"tag_name": "notes/beta.014",
|
||||
"name": "Sanctuary beta.014 — Rangement, hotbar mobile et préfixe de faction",
|
||||
"body_file": "notes/beta.014.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "80071335c063edec5f76059d839c7904fd24bb21641f31d029cf25b88e0efdd6"
|
||||
},
|
||||
{
|
||||
"version": "beta.015",
|
||||
"tag_name": "notes/beta.015",
|
||||
"name": "Sanctuary beta.015 — Icônes de progression communes",
|
||||
"body_file": "notes/beta.015.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "f0ea350fc5710608b445494a8f229cf7453616e27c3fe08a880a14ca087153d5"
|
||||
},
|
||||
{
|
||||
"version": "beta.016",
|
||||
"tag_name": "notes/beta.016",
|
||||
"name": "Sanctuary beta.016 — Les premiers gestes rapportent de l’XP",
|
||||
"body_file": "notes/beta.016.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "2f049fcfa96f69b4ad77832ee21d9fa897e412a7766628479dcbb9229bf3dc15"
|
||||
},
|
||||
{
|
||||
"version": "beta.017",
|
||||
"tag_name": "notes/beta.017",
|
||||
"name": "Sanctuary beta.017 — Nage, S’allonger, Se reposer et fabrication manuelle",
|
||||
"body_file": "notes/beta.017.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "75f45ac8203a8be496bf56f20641e5d900dcb6d656004641ab166a975b248a98"
|
||||
},
|
||||
{
|
||||
"version": "beta.018",
|
||||
"tag_name": "notes/beta.018",
|
||||
"name": "Sanctuary beta.018 — Familiers, cape et cosmétique de tête",
|
||||
"body_file": "notes/beta.018.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "af5d7f2a9a08be017e9ea565e0ac7b7f89a5668eaca752bbef3fd83d4dcd1b83"
|
||||
},
|
||||
{
|
||||
"version": "beta.019",
|
||||
"tag_name": "notes/beta.019",
|
||||
"name": "Sanctuary beta.019 — Pouvoirs et déplacements des familiers",
|
||||
"body_file": "notes/beta.019.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "89014f518126d23a927f385e39e0e45bac1862b753eedd5dbf6a982f2fc02ae8"
|
||||
},
|
||||
{
|
||||
"version": "beta.020",
|
||||
"tag_name": "notes/beta.020",
|
||||
"name": "Sanctuary beta.020 — Œufs singuliers, portage et plongeon",
|
||||
"body_file": "notes/beta.020.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "a40be9d43b2e706d96b051a801445ce623889c36963f4a9a1114c3275dab1406"
|
||||
},
|
||||
{
|
||||
"version": "beta.022",
|
||||
"tag_name": "notes/beta.022",
|
||||
"name": "Sanctuary beta.022 — Création de monde plus rapide",
|
||||
"body_file": "notes/beta.022.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "20c29abb7f12024956084f5ad5c384306a0beb57b6f49e5108dd01cad16a3e71"
|
||||
},
|
||||
{
|
||||
"version": "beta.023",
|
||||
"tag_name": "notes/beta.023",
|
||||
"name": "Sanctuary beta.023 — Cosmétiques directement sur leur case",
|
||||
"body_file": "notes/beta.023.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "56290132ee6abb0992102e7b3bc0320298390e56a4055f7f36a49986209b356c"
|
||||
},
|
||||
{
|
||||
"version": "beta.024",
|
||||
"tag_name": "notes/beta.024",
|
||||
"name": "Sanctuary beta.024 — Recettes progressives dans le Blocodex",
|
||||
"body_file": "notes/beta.024.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "92e5de8ae776f91a5ba5f3d0a884fea7e1f81811a83b2a712bf234c08293b4bb"
|
||||
},
|
||||
{
|
||||
"version": "beta.025",
|
||||
"tag_name": "notes/beta.025",
|
||||
"name": "Sanctuary beta.025 — Barres d’XP des compétences",
|
||||
"body_file": "notes/beta.025.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "cbd9efc6d630a0ef2a1511c489545e9e87c8bd79b836b7616ade9ac077f0f8eb"
|
||||
},
|
||||
{
|
||||
"version": "beta.026",
|
||||
"tag_name": "notes/beta.026",
|
||||
"name": "Sanctuary beta.026 — Cache d’affichage Demeure sur la carte",
|
||||
"body_file": "notes/beta.026.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "aee2d4e998dd2ac9dc172d79018e552c658bd66db217f55b94e5bdd6bcabd50b"
|
||||
},
|
||||
{
|
||||
"version": "beta.027",
|
||||
"tag_name": "notes/beta.027",
|
||||
"name": "Sanctuary beta.027 — Piles de joueurs",
|
||||
"body_file": "notes/beta.027.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "9ec554eebe10b8b35955fbebd1404a273b26f8fe3693aed1b2407305cf7c7699"
|
||||
},
|
||||
{
|
||||
"version": "beta.028",
|
||||
"tag_name": "notes/beta.028",
|
||||
"name": "Sanctuary beta.028 — Plongeon jusqu’à collision",
|
||||
"body_file": "notes/beta.028.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "af533c1dc914aeab9e967d07dc8a419a7d039e1ab78365221ac90aae69a2142d"
|
||||
},
|
||||
{
|
||||
"version": "beta.029",
|
||||
"tag_name": "notes/beta.029",
|
||||
"name": "Sanctuary beta.029 — Cri et recul des familiers",
|
||||
"body_file": "notes/beta.029.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "bf653938f97f056493b17666cb2e5504db244cb2f114ebbd4fe581b48b20c53b"
|
||||
},
|
||||
{
|
||||
"version": "beta.030",
|
||||
"tag_name": "notes/beta.030",
|
||||
"name": "Sanctuary beta.030 — Lumières portées et objets jetés",
|
||||
"body_file": "notes/beta.030.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "603e95e406ebf286c1b029d97e18b389f1d0135e1c92374dcdaa927b49ead93c"
|
||||
},
|
||||
{
|
||||
"version": "beta.031",
|
||||
"tag_name": "notes/beta.031",
|
||||
"name": "Sanctuary beta.031 — Apparition galactique",
|
||||
"body_file": "notes/beta.031.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "700426014052a0c44f4a7e360912dae6c1e8c80379cfe298ba5f8c5575cd6efe"
|
||||
},
|
||||
{
|
||||
"version": "beta.032",
|
||||
"tag_name": "notes/beta.032",
|
||||
"name": "Sanctuary beta.032 — Refonte des 88 familiers",
|
||||
"body_file": "notes/beta.032.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "b24ef127919fafdb71d5b0b79b8ed6419b786cfd8b74aed6244b3b4527e6af3f"
|
||||
},
|
||||
{
|
||||
"version": "beta.033",
|
||||
"tag_name": "notes/beta.033",
|
||||
"name": "Sanctuary beta.033 — Menus directs et profil de test rapide",
|
||||
"body_file": "notes/beta.033.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "865724dab2959885f19f39d479b2f49218c4001a818c304c1ef200f30412ccf8"
|
||||
},
|
||||
{
|
||||
"version": "beta.034",
|
||||
"tag_name": "notes/beta.034",
|
||||
"name": "Sanctuary beta.034 — Fil de découvertes et hotbar agrandie",
|
||||
"body_file": "notes/beta.034.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "07afd3e8dd1b82aa515bb0db789975d34b9d44e6ae00f5213b8ea29912e41367"
|
||||
},
|
||||
{
|
||||
"version": "beta.035",
|
||||
"tag_name": "notes/beta.035",
|
||||
"name": "Sanctuary beta.035 — Collections Minecraft",
|
||||
"body_file": "notes/beta.035.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "5b705cd4160907d649f5888130432d390dbd4bc28d3fe5aabf6234feaac30cc6"
|
||||
},
|
||||
{
|
||||
"version": "beta.036",
|
||||
"tag_name": "notes/beta.036",
|
||||
"name": "Sanctuary beta.036 — Notifications et suivi choisi",
|
||||
"body_file": "notes/beta.036.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "3e18002138675b1e7d3f80ae633a49cc9204a32c683405dc1ef2a887d1da5810"
|
||||
},
|
||||
{
|
||||
"version": "beta.037",
|
||||
"tag_name": "notes/beta.037",
|
||||
"name": "Sanctuary beta.037 — Temps réel et suivi commun",
|
||||
"body_file": "notes/beta.037.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "a4dc686f2bea49c80a001c99b3c5e42d77bda5227223ba5e4d2a09d928915180"
|
||||
},
|
||||
{
|
||||
"version": "beta.038",
|
||||
"tag_name": "notes/beta.038",
|
||||
"name": "Sanctuary beta.038 — Nourriture, têtes-tombes et factions",
|
||||
"body_file": "notes/beta.038.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "41d70130039d2fc5bf24fc761757a82223ed7c64bdf209371a2ce1455b2a107b"
|
||||
},
|
||||
{
|
||||
"version": "beta.039",
|
||||
"tag_name": "notes/beta.039",
|
||||
"name": "Sanctuary beta.039 — Hello World, savoir alimentaire et inventaires de mort",
|
||||
"body_file": "notes/beta.039.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "11eeb112aef244c42253a088a407c81513bad569f1a006ea85ff8c37a87c71da"
|
||||
},
|
||||
{
|
||||
"version": "beta.040",
|
||||
"tag_name": "notes/beta.040",
|
||||
"name": "Sanctuary beta.040 — Mode opérateur, fiche Habitant et ciel partagé",
|
||||
"body_file": "notes/beta.040.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "f4ba5e5510fef9200b5a0d1cf0935ef6bb431b334caf55d59566873cdfa068cd"
|
||||
},
|
||||
{
|
||||
"version": "beta.041",
|
||||
"tag_name": "notes/beta.041",
|
||||
"name": "Sanctuary beta.041 — Portage des bébés",
|
||||
"body_file": "notes/beta.041.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "e9d46eb29e305c31e3826c4d65f76ca1666704c75bcef5ccbe43720e5409b8fe"
|
||||
},
|
||||
{
|
||||
"version": "beta.042",
|
||||
"tag_name": "notes/beta.042",
|
||||
"name": "Sanctuary beta.042 — Joueurs assis sur la tête",
|
||||
"body_file": "notes/beta.042.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "bb1937a1ce6b9f559921ec0a70042eea8b5333f4377cf208559d951e6cb2ff04"
|
||||
},
|
||||
{
|
||||
"version": "beta.043",
|
||||
"tag_name": "notes/beta.043",
|
||||
"name": "Sanctuary beta.043 — Blocs portés et icône officielle",
|
||||
"body_file": "notes/beta.043.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "335aac4865234c699ab0210ec2eba76453e5865b23ec34fba319c3e1cd29f4f9"
|
||||
},
|
||||
{
|
||||
"version": "beta.044",
|
||||
"tag_name": "notes/beta.044",
|
||||
"name": "Sanctuary beta.044 — Pouvoirs natifs des familiers",
|
||||
"body_file": "notes/beta.044.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "4fa2312d8b24495a19f6237ddea2db6df5ff830887936900bc6673d34859fb43"
|
||||
},
|
||||
{
|
||||
"version": "beta.045",
|
||||
"tag_name": "notes/beta.045",
|
||||
"name": "Sanctuary beta.045 — Menu principal",
|
||||
"body_file": "notes/beta.045.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "963ac6b973b2dec3a6d7874a33085c35d76ed5b6b9e3203e87b46d859ad60f3a"
|
||||
},
|
||||
{
|
||||
"version": "beta.046",
|
||||
"tag_name": "notes/beta.046",
|
||||
"name": "Sanctuary beta.046 — Constellations, vol et zoom",
|
||||
"body_file": "notes/beta.046.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "dab6dda881a19155e56543c9a23cb2c0081f9e4bb5cd16c8ff36a3cca70ece0a"
|
||||
},
|
||||
{
|
||||
"version": "beta.047",
|
||||
"tag_name": "notes/beta.047",
|
||||
"name": "Sanctuary beta.047 — Étoiles à la longue-vue et boutons",
|
||||
"body_file": "notes/beta.047.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "bc62848c2d43d78898ebed20beb7011e2dc006434edab6202c1a2d9b73605ecb"
|
||||
},
|
||||
{
|
||||
"version": "beta.048",
|
||||
"tag_name": "notes/beta.048",
|
||||
"name": "Sanctuary beta.048 — Bateaux collectifs et moteurs animaux",
|
||||
"body_file": "notes/beta.048.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "67fb7645ea42cc1ba5bc57167ea241dca10f77cead976bf055acb29d596dd773"
|
||||
},
|
||||
{
|
||||
"version": "beta.049",
|
||||
"tag_name": "notes/beta.049",
|
||||
"name": "Sanctuary beta.049 — Bateaux rectangulaires et chapeaux actifs",
|
||||
"body_file": "notes/beta.049.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "4c4a60abae6821ba0fa790abd90ff0ccf87f838b19507cf620e840c2da6b3915"
|
||||
},
|
||||
{
|
||||
"version": "beta.050",
|
||||
"tag_name": "notes/beta.050",
|
||||
"name": "Sanctuary beta.050 — Bateaux compacts et redstone portée",
|
||||
"body_file": "notes/beta.050.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6e5a697160c8696017ffcf946934d030dea5dc86982f70612ec2ee2bfb1e497d"
|
||||
},
|
||||
{
|
||||
"version": "beta.051",
|
||||
"tag_name": "notes/beta.051",
|
||||
"name": "Sanctuary beta.051 — Distributeur porté orienté par le regard",
|
||||
"body_file": "notes/beta.051.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "087d760d64ebe1ff6b948080e8e47583101c28c31c6e61ed1c6cb1713cd1bfc5"
|
||||
},
|
||||
{
|
||||
"version": "beta.052",
|
||||
"tag_name": "notes/beta.052",
|
||||
"name": "Sanctuary beta.052 — Nom du familier de départ",
|
||||
"body_file": "notes/beta.052.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "9039530f3a89ee84a36883a7ddcf631029400b698ac480355c814bf8fa83be0b"
|
||||
},
|
||||
{
|
||||
"version": "beta.053",
|
||||
"tag_name": "notes/beta.053",
|
||||
"name": "Sanctuary beta.053 — Brume progressive et saturation",
|
||||
"body_file": "notes/beta.053.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "f8cb565c3f91c9c02a364557acb89fd38a85c90d8e7f60e867959cc66cbbe2d6"
|
||||
},
|
||||
{
|
||||
"version": "beta.054",
|
||||
"tag_name": "notes/beta.054",
|
||||
"name": "Sanctuary beta.054 — Duels, mises et niveaux des familiers",
|
||||
"body_file": "notes/beta.054.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "d402b014be15f7bcce2e0cd3e570f06aedd0fb0bb3ce03896b86654d53f70346"
|
||||
},
|
||||
{
|
||||
"version": "beta.055",
|
||||
"tag_name": "notes/beta.055",
|
||||
"name": "Sanctuary beta.055 — Force et portage de joueurs",
|
||||
"body_file": "notes/beta.055.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "858439b89ffce6ccd712bbe490fc795ef8dd57ea2de7a02d7ce81a6e0e7f150c"
|
||||
},
|
||||
{
|
||||
"version": "beta.056",
|
||||
"tag_name": "notes/beta.056",
|
||||
"name": "Sanctuary beta.056 — Étoiles dispersées et saturation",
|
||||
"body_file": "notes/beta.056.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "59db3c7369bad4ac4553e99df7af9f1a7740c990be11e1c5348b0e02eb54109e"
|
||||
},
|
||||
{
|
||||
"version": "beta.057",
|
||||
"tag_name": "notes/beta.057",
|
||||
"name": "Sanctuary beta.057 — Météo quotidienne et nouvelles ambiances",
|
||||
"body_file": "notes/beta.057.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "d9f72880eb257b40711f85163f9ea5e16f74c7fb161482e9f65d66b37fafcbd7"
|
||||
},
|
||||
{
|
||||
"version": "beta.058",
|
||||
"tag_name": "notes/beta.058",
|
||||
"name": "Sanctuary beta.058 — Altitude de la carte",
|
||||
"body_file": "notes/beta.058.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "690d0e236dcc4bef1ba3ce888224e3650dda38076496994157908c674f84b325"
|
||||
},
|
||||
{
|
||||
"version": "beta.059",
|
||||
"tag_name": "notes/beta.059",
|
||||
"name": "Sanctuary beta.059 — Bannières et cartes au trésor",
|
||||
"body_file": "notes/beta.059.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "367ee855927b0f58c5b21daa67611d4c07f951d05edb3b088a60994193bdc873"
|
||||
},
|
||||
{
|
||||
"version": "beta.060",
|
||||
"tag_name": "beta.060",
|
||||
"name": "Sanctuary beta.060 — Objets et blocs sur les mobs",
|
||||
"body_file": "notes/beta.060.md",
|
||||
"source_tag_exact": true,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "4ea89295586862e5b549e9ed386f4467b9816290380e2d26c84765eecf186ded"
|
||||
},
|
||||
{
|
||||
"version": "beta.061",
|
||||
"tag_name": "beta.061",
|
||||
"name": "Sanctuary beta.061 — Musique d’arrivée et montures familières",
|
||||
"body_file": "notes/beta.061.md",
|
||||
"source_tag_exact": true,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "410bc6d3b4a01133d4ebb653e7923216bd615584a148c1711d009f681d1d5616"
|
||||
},
|
||||
{
|
||||
"version": "beta.062",
|
||||
"tag_name": "notes/beta.062",
|
||||
"name": "Sanctuary beta.062 — Soleil et chapeaux des familiers",
|
||||
"body_file": "notes/beta.062.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "a082002465fb10d1c9e133270e9ec96ade18ec087703b1e7e3cbfd57f06d0a12"
|
||||
},
|
||||
{
|
||||
"version": "beta.063",
|
||||
"tag_name": "notes/beta.063",
|
||||
"name": "Sanctuary beta.063 — Chargement et règle de jeu Sanctuary",
|
||||
"body_file": "notes/beta.063.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "84e009e16741375fc36a01d54d33476c0f0f9559a0dc3eb146549d9326b579c7"
|
||||
},
|
||||
{
|
||||
"version": "beta.064",
|
||||
"tag_name": "notes/beta.064",
|
||||
"name": "Sanctuary beta.064 — Cache et étapes réelles de préparation",
|
||||
"body_file": "notes/beta.064.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "40073089e63e58e1c5f1a994ab17e3dee5d4869cbb00e33c1a63d7d204c6d015"
|
||||
}
|
||||
]
|
||||
@@ -1,687 +0,0 @@
|
||||
plugins {
|
||||
id 'net.fabricmc.fabric-loom'
|
||||
}
|
||||
|
||||
group = rootProject.maven_group
|
||||
version = rootProject.mod_version
|
||||
base { archivesName = 'sanctuary' }
|
||||
|
||||
def clientRenderTests = providers.gradleProperty('sanctuaryClientTests')
|
||||
.map { it.toBoolean() }.getOrElse(false)
|
||||
|
||||
fabricApi {
|
||||
configureTests {
|
||||
createSourceSet = true
|
||||
modId = 'sanctuary-gametest'
|
||||
enableGameTests = true
|
||||
// Explicit opt-in: the rendering check needs a graphics-capable client.
|
||||
enableClientGameTests = clientRenderTests
|
||||
eula = false
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
tasks.register('cleanGameTestWorld', Delete) {
|
||||
onlyIf { !providers.gradleProperty('sanctuaryExpansionReload').map { it.toBoolean() }.getOrElse(false) }
|
||||
delete(layout.buildDirectory.dir('run/gameTest/world'))
|
||||
}
|
||||
tasks.named('runGameTest') {
|
||||
dependsOn('cleanGameTestWorld')
|
||||
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'))
|
||||
systemProperty('sanctuary.test.densityMaps', providers.gradleProperty('sanctuaryDensityMaps').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false) ? '17' : providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false) ? '19' : (providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)) ? '20' : (providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)) ? '21' : providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false) ? '22' : ['sanctuarySecrets23Tests', 'sanctuaryNative23Tests', 'sanctuaryOcean23Tests', 'sanctuaryAdmission23Tests'].any { providers.gradleProperty(it).map { v -> v.toBoolean() }.getOrElse(false) } ? '23' : '24'))
|
||||
systemProperty('sanctuary.test.aerial24.flowAdmission', providers.gradleProperty('sanctuaryAerial24FlowAdmission').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24', providers.gradleProperty('sanctuaryAerial24Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24.reverseChunks', providers.gradleProperty('sanctuaryAerial24ReverseChunks').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.aerial24.phase', providers.gradleProperty('sanctuaryAerial24Phase').getOrElse('create'))
|
||||
systemProperty('sanctuary.test.secrets23', providers.gradleProperty('sanctuarySecrets23Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.admission23', providers.gradleProperty('sanctuaryAdmission23Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.admission23.reserveOnly', providers.gradleProperty('sanctuaryAdmission23ReserveOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.admission23.diameter', providers.gradleProperty('sanctuaryAdmission23Diameter').getOrElse('256'))
|
||||
systemProperty('sanctuary.test.admission23.climate', providers.gradleProperty('sanctuaryAdmission23Climate').getOrElse('temperate'))
|
||||
systemProperty('sanctuary.test.admission23.relief', providers.gradleProperty('sanctuaryAdmission23Relief').getOrElse('natural'))
|
||||
systemProperty('sanctuary.test.ocean23', providers.gradleProperty('sanctuaryOcean23Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.native23', providers.gradleProperty('sanctuaryNative23Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.generateStructures', providers.gradleProperty('sanctuaryGenerateStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.transit29', providers.gradleProperty('sanctuaryTransit29Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.transit29.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.requireMineshaft29', providers.gradleProperty('sanctuaryRequireMineshaft29').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.requireCurvedRiverBank', providers.gradleProperty('sanctuaryRequireCurvedRiverBank').getOrElse('false'))
|
||||
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity20', providers.gradleProperty('sanctuaryLostCity20Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage20', providers.gradleProperty('sanctuaryHeritage20Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.heritage19', providers.gradleProperty('sanctuaryHeritage19Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.voidReuse', providers.gradleProperty('sanctuaryVoidReuseTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.optionalStructureWitness', providers.gradleProperty('sanctuaryOptionalStructureWitness').getOrElse('false'))
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('benchmark', benchmark)
|
||||
def unified = providers.gradleProperty('sanctuaryUnifiedTests').map { it.toBoolean() }.getOrElse(true)
|
||||
inputs.property('unified', unified)
|
||||
def expansion = providers.gradleProperty('sanctuaryExpansionTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('expansion', expansion)
|
||||
def lostCity = providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity', lostCity)
|
||||
def lostCity15 = providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity15', lostCity15)
|
||||
def lostCity16 = providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity16', lostCity16)
|
||||
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('ruins17', ruins17)
|
||||
def lostCity20 = providers.gradleProperty('sanctuaryLostCity20Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity20', lostCity20)
|
||||
def heritage20 = providers.gradleProperty('sanctuaryHeritage20Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage20', heritage20)
|
||||
def heritage21 = providers.gradleProperty('sanctuaryHeritage21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage21', heritage21)
|
||||
def lostCity21 = providers.gradleProperty('sanctuaryLostCity21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity21', lostCity21)
|
||||
def aerial24Flow = providers.gradleProperty('sanctuaryAerial24FlowAdmission').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('aerial24Flow', aerial24Flow)
|
||||
def aerial24 = providers.gradleProperty('sanctuaryAerial24Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('aerial24', aerial24)
|
||||
def secrets23 = providers.gradleProperty('sanctuarySecrets23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
def admission23 = providers.gradleProperty('sanctuaryAdmission23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('admission23', admission23)
|
||||
def ocean23 = providers.gradleProperty('sanctuaryOcean23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('ocean23', ocean23)
|
||||
def native23 = providers.gradleProperty('sanctuaryNative23Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('secrets23', secrets23)
|
||||
inputs.property('native23', native23)
|
||||
def selective22 = providers.gradleProperty('sanctuarySelective22Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('selective22', selective22)
|
||||
def transit21 = providers.gradleProperty('sanctuaryTransit21Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('transit21', transit21)
|
||||
def heritage19 = providers.gradleProperty('sanctuaryHeritage19Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('heritage19', heritage19)
|
||||
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
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release = 25
|
||||
}
|
||||
|
||||
processResources {
|
||||
// The launcher uses 26.3-pre-2; Fabric's runtime normalizes it to 26.3-pre.2.
|
||||
def values = [version: project.version, minecraft_version: rootProject.minecraft_version,
|
||||
minecraft_dependency_version: rootProject.minecraft_version.replace('-pre-', '-pre.'),
|
||||
loader_version: rootProject.loader_version, fabric_api_version: rootProject.fabric_api_version]
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
from(rootProject.file('LICENSE')) { rename { 'LICENSE_sanctuary' } }
|
||||
from(rootProject.file('THIRD_PARTY_NOTICES.md'))
|
||||
}
|
||||
|
||||
tasks.register('worldgenSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check natural central holes, continuous erosion, density bounds and exterior void.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.WorldgenSmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('worldgenSmoke') }
|
||||
|
||||
tasks.register('hydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check deterministic surface drainage, natural support and bounded excavation.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('hydrologySmoke') }
|
||||
|
||||
// src/test contains a JavaExec assertion harness; engine tests use Fabric's gametest source set.
|
||||
tasks.named('test') { failOnNoDiscoveredTests = false }
|
||||
|
||||
tasks.register('blockKnowledgeSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check personal block history, atomic persistence and corrupt-file refusal.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.knowledge.BlockKnowledgeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('blockKnowledgeSmoke') }
|
||||
|
||||
tasks.register('materialActivitySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check dated material observations, daily isolation and atomic persistence.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.knowledge.history.MaterialActivitySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('materialActivitySmoke') }
|
||||
|
||||
tasks.register('naturalHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check broad shores, supported sediments and natural rock springs.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.NaturalHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('naturalHydrologySmoke') }
|
||||
|
||||
tasks.register('starterLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check a small contained lava niche in naturally supported rock.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.StarterLavaDepositSmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('starterLavaSmoke') }
|
||||
|
||||
// Alpha.6 has separate contracts so saved alpha.5 worlds keep their generation.
|
||||
tasks.register('layeredHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check retained basins, broad rock shores and lower wall springs.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.LayeredHydrologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('layeredHydrologySmoke') }
|
||||
|
||||
tasks.register('layeredEcologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check altitude biome variation and naturally supported lower terraces.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.LayeredEcologySmoke'
|
||||
}
|
||||
|
||||
tasks.named('check') { dependsOn('layeredEcologySmoke') }
|
||||
|
||||
// Alpha.7 keeps previous biome palettes and generators unchanged.
|
||||
tasks.register('woodlandPaletteSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check surface forest coverage and bounded rare interior biomes.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.WoodlandPaletteSmoke'
|
||||
}
|
||||
tasks.register('woodlandGroveSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check natural tree supports and canopy space without creating terrain.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.WoodlandGroveSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('woodlandPaletteSmoke', 'woodlandGroveSmoke') }
|
||||
|
||||
tasks.register('riftShapeSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftShapeSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftShapeSmoke') }
|
||||
|
||||
tasks.register('riftPaletteSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftPaletteSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftPaletteSmoke') }
|
||||
|
||||
tasks.register('riftHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftHydrologySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftHydrologySmoke') }
|
||||
|
||||
tasks.register('riftLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.RiftLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('riftLavaSmoke') }
|
||||
|
||||
tasks.register('cavernPaletteSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernPaletteSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernPaletteSmoke') }
|
||||
|
||||
tasks.register('cavernHydrologySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernHydrologySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernHydrologySmoke') }
|
||||
|
||||
tasks.register('cavernLavaSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavernLavaSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavernLavaSmoke') }
|
||||
|
||||
tasks.register('populationCapacitySmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check supported capacities, area scaling, finite boundaries and regional ownership.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationCapacitySmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationCapacitySmoke') }
|
||||
|
||||
tasks.register('populationRegionPlanSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check atomic regional admission, hydraulic dependencies and translated chunk indices.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationRegionPlanSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationRegionPlanSmoke') }
|
||||
|
||||
tasks.register('populationPlanCacheSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check that completed regions remain readable while new regions are planned.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationPlanCacheSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationPlanCacheSmoke') }
|
||||
|
||||
tasks.register('expansionJournalSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionJournalSmoke'
|
||||
}
|
||||
tasks.register('expansionReliefSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionReliefSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionJournalSmoke', 'expansionReliefSmoke') }
|
||||
|
||||
tasks.register('expansionPlacementSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionPlacementSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionPlacementSmoke') }
|
||||
|
||||
// Alpha.14 planner uses bounded natural support and never queries loaded chunks.
|
||||
tasks.register('lostCityPlannerSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityPlannerSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityPlannerSmoke') }
|
||||
|
||||
tasks.register('lostCityRendererSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityRendererSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityRendererSmoke') }
|
||||
|
||||
tasks.register('expansionSpatial14Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionSpatialPlacement14Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionSpatial14Smoke') }
|
||||
|
||||
tasks.register('expansionVoidCertificateSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionVoidCertificateSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionVoidCertificateSmoke') }
|
||||
|
||||
tasks.register('lostCityInteriors15Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityInteriors15Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityInteriors15Smoke') }
|
||||
|
||||
tasks.register('lostCityUnderground15Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityUnderground15Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityUnderground15Smoke') }
|
||||
|
||||
tasks.register('lostCityProtection15Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check bounded three-dimensional city protection and immutable snapshots.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityProtection15Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityProtection15Smoke') }
|
||||
|
||||
tasks.register('lostCityPlanner15Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check terrain-certified city fragments, inset floors and anchored viaducts.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityPlanner15Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityPlanner15Smoke') }
|
||||
|
||||
tasks.register('lostCityInteriors16Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityInteriors16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityInteriors16Smoke') }
|
||||
|
||||
tasks.register('lostCityUnderground16Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityUnderground16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityUnderground16Smoke') }
|
||||
|
||||
tasks.register('lostCityProtection16Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check bounded three-dimensional city protection and immutable snapshots.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityProtection16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityProtection16Smoke') }
|
||||
|
||||
tasks.register('lostCityPlanner16Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check terrain-certified city fragments, inset floors and anchored viaducts.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityPlanner16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityPlanner16Smoke') }
|
||||
|
||||
|
||||
tasks.register('lostCityFoundation16Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityFoundation16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
|
||||
|
||||
// The experimental pieces and algorithms are additive; historical city suites remain unchanged.
|
||||
['Planner', 'Renderer', 'Piece'].each { component ->
|
||||
def smoke = tasks.register("ruins17${component}Smoke", JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.ruins.Ruins${component}17Smoke"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Local native starts keep the long heritage routes bounded by Minecraft's reference range.
|
||||
['Planner', 'Renderer', 'Piece'].each { component ->
|
||||
def smoke = tasks.register("heritage19${component}Smoke", JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.heritage.Heritage${component}19Smoke"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Generation20 architecture retains the previous renderers and verifies connected block states.
|
||||
[
|
||||
connectedBlocks20Smoke: 'detail.ConnectedBlocks20Smoke',
|
||||
heritage20RendererSmoke: 'heritage.HeritageRenderer20Smoke',
|
||||
heritage20PieceSmoke: 'heritage.HeritagePiece20Smoke',
|
||||
ruins20RendererSmoke: 'ruins.RuinsRenderer20Smoke',
|
||||
lostCityUnderground20Smoke: 'city.LostCityUnderground20Smoke',
|
||||
lostCitySurface20Smoke: 'city.LostCitySurface20Smoke',
|
||||
lostCityInteriors20Smoke: 'city.LostCityInteriors20Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// Optional actual block-state exports for architectural review; never enabled in ordinary checks.
|
||||
tasks.named('heritage20RendererSmoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.test.heritage20.export', rootProject.layout.buildDirectory.dir('alpha20-preview/heritage').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCityInteriors20Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city20.preview', rootProject.layout.buildDirectory.file('alpha20-preview/city.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCitySurface20Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city20.exteriorPreview', rootProject.layout.buildDirectory.file('alpha20-preview/exterior.json').get().asFile.absolutePath)
|
||||
}
|
||||
|
||||
// Generation21 keeps published20 renderers immutable.
|
||||
[
|
||||
heritage21RendererSmoke: 'heritage.HeritageRenderer21Smoke',
|
||||
heritage21PieceSmoke: 'heritage.HeritagePiece21Smoke',
|
||||
heritage21PlannerSmoke: 'heritage.HeritagePlanner21Smoke',
|
||||
lostCity21PlannerSmoke: 'city.LostCityPlanner21Smoke',
|
||||
lostCitySurface21Smoke: 'city.LostCitySurface21Smoke',
|
||||
lostCityInteriors21Smoke: 'city.LostCityInteriors21Smoke',
|
||||
transit21Smoke: 'transit.Transit21Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
tasks.named('heritage21RendererSmoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.test.heritage21.export', rootProject.layout.buildDirectory.dir('alpha21-preview/heritage').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCityInteriors21Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city21.preview', rootProject.layout.buildDirectory.file('alpha21-preview/city.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('lostCitySurface21Smoke') {
|
||||
if (providers.gradleProperty('sanctuaryArchitecturePreview').isPresent())
|
||||
systemProperty('sanctuary.city21.exteriorPreview', rootProject.layout.buildDirectory.file('alpha21-preview/exterior.json').get().asFile.absolutePath)
|
||||
}
|
||||
|
||||
// Generation22 filters surface placement and adds an independent core hall; historical tests still run.
|
||||
[
|
||||
lostCity22PlannerSmoke: 'city.LostCityPlanner22Smoke',
|
||||
underground22Smoke: 'city.LostCityUnderground22Smoke',
|
||||
heritage22PlannerSmoke: 'heritage.HeritagePlanner22Smoke',
|
||||
transit22PlannerSmoke: 'transit.TransitPlanner22Smoke',
|
||||
transit22RouteSmoke: 'transit.TransitRoute22Smoke',
|
||||
transit22RendererSmoke: 'transit.TransitRenderer22Smoke'
|
||||
].each { name, harness ->
|
||||
def smoke = tasks.register(name, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.${harness}"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
// New geography has a pure harness in addition to native world witnesses.
|
||||
tasks.register('expansionOcean23Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.ExpansionOcean23Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionOcean23Smoke') }
|
||||
|
||||
[
|
||||
secrets23Smoke: 'fr.koka.sanctuary.worldgen.mineral.Secrets23Smoke',
|
||||
structureCensusSmoke: 'fr.koka.sanctuary.knowledge.structures.StructureCensusSmoke',
|
||||
expansionPriorities23Smoke: 'fr.koka.sanctuary.expansion.ExpansionPriorities23Smoke'
|
||||
].each { taskName, harness ->
|
||||
tasks.register(taskName, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = harness
|
||||
}
|
||||
tasks.named('check') { dependsOn(taskName) }
|
||||
}
|
||||
|
||||
// Versioned aerial planner: bounded search, native pieces and local claims.
|
||||
tasks.register('aerial24Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
|
||||
if (providers.gradleProperty('sanctuaryAerialPreview').isPresent())
|
||||
systemProperty('sanctuary.aerial.preview', rootProject.layout.buildDirectory.file('alpha27-preview/native-ships.json').get().asFile.absolutePath)
|
||||
}
|
||||
tasks.named('check') { dependsOn('aerial24Smoke') }
|
||||
|
||||
tasks.register('aerialPersistence24Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
|
||||
|
||||
tasks.register('swamp27VolumesSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
|
||||
|
||||
tasks.register('cavesRelief28Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.CavesRelief28Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cavesRelief28Smoke') }
|
||||
|
||||
// Fine ledges retain the alpha28 field below the upper relief.
|
||||
tasks.register('terraces29Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Terraces29Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('terraces29Smoke') }
|
||||
|
||||
[
|
||||
transit29Smoke: 'fr.koka.sanctuary.worldgen.transit.Transit29Smoke',
|
||||
mineshaftAdmission29Smoke: 'fr.koka.sanctuary.expansion.MineshaftAdmission29Smoke'
|
||||
].each { taskName, harness ->
|
||||
tasks.register(taskName, JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = harness
|
||||
}
|
||||
tasks.named('check') { dependsOn(taskName) }
|
||||
}
|
||||
|
||||
tasks.register('river30Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('river30Smoke') }
|
||||
|
||||
tasks.register('surfaceRelief302Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceRelief302Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('surfaceRelief302Smoke') }
|
||||
|
||||
tasks.register('balancedRelief303Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn tasks.named('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.BalancedRelief303Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('balancedRelief303Smoke') }
|
||||
-295
@@ -1,295 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.client.SanctuaryBlocodexScreen;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledge;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgePayloads;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.CycleButton;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.ObjectSelectionList;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Native rendering with an explicit test fixture; no world or server state is fabricated. */
|
||||
final class BlocodexClientChecks {
|
||||
private static final KeyEvent PRESS = new KeyEvent(InputConstants.KEY_RETURN, 0, 0);
|
||||
private static final String LARGE_COUNTER_ID = "minecraft:ancient_debris";
|
||||
private static final String REMOVED_MOD_ID = "fixture_missing_mod:retained_block";
|
||||
|
||||
static void run(ClientGameTestContext context) {
|
||||
Screen previous = context.computeOnClient(client -> client.gui.screen());
|
||||
int previousScale = context.computeOnClient(client -> client.options.guiScale().get());
|
||||
List<BlockKnowledge> fixture = context.computeOnClient(client -> fixture());
|
||||
List<Map<String, Object>> screenshots = new ArrayList<>();
|
||||
try {
|
||||
context.runOnClient(client -> {
|
||||
client.options.guiScale().set(1);
|
||||
client.resizeGui();
|
||||
client.gui.setScreen(new SanctuaryBlocodexScreen());
|
||||
});
|
||||
SanctuaryBlocodexScreen screen = context.computeOnClient(client -> (SanctuaryBlocodexScreen) client.gui.screen());
|
||||
context.runOnClient(client -> {
|
||||
require(list(screen).children().isEmpty(), "No connection must not invent unknown block rows");
|
||||
fixtureRequest(screen, 401);
|
||||
screen.accept(wireRoundTrip(part(fixture, 999, 0)));
|
||||
require(list(screen).children().isEmpty(), "A stale request must not populate the screen");
|
||||
screen.accept(wireRoundTrip(part(fixture, 401, 0)));
|
||||
require(list(screen).children().isEmpty(), "An incomplete snapshot must not reveal partial knowledge");
|
||||
for (int index = 1; index < partCount(fixture); index++) {
|
||||
screen.accept(wireRoundTrip(part(fixture, 401, index)));
|
||||
}
|
||||
require(filteredCount(screen) == fixture.size(), "The full snapshot must become visible together");
|
||||
verifyControls(screen, fixture);
|
||||
assertWidgetBounds(screen);
|
||||
});
|
||||
context.waitTick();
|
||||
Path large = context.takeScreenshot("sanctuary-blocodex-fixture-large");
|
||||
screenshots.add(context.computeOnClient(client -> Map.of("file", large.toString(),
|
||||
"gui_width", screen.width, "gui_height", screen.height, "gui_scale", client.getWindow().getGuiScale())));
|
||||
|
||||
context.runOnClient(client -> {
|
||||
// Vanilla auto scale chooses its largest permitted scale and therefore its smallest GUI.
|
||||
client.options.guiScale().set(0);
|
||||
client.resizeGui();
|
||||
require(client.gui.screen() == screen, "Changing GUI scale must retain the Blocodex");
|
||||
require(filteredCount(screen) == fixture.size(), "Resizing must retain the loaded catalogue");
|
||||
assertWidgetBounds(screen);
|
||||
verifyControls(screen, fixture);
|
||||
});
|
||||
context.waitTick();
|
||||
Path small = context.takeScreenshot("sanctuary-blocodex-fixture-minimum-gui");
|
||||
screenshots.add(context.computeOnClient(client -> Map.of("file", small.toString(),
|
||||
"gui_width", screen.width, "gui_height", screen.height, "gui_scale", client.getWindow().getGuiScale())));
|
||||
context.runOnClient(client -> {
|
||||
search(screen).setValue(LARGE_COUNTER_ID);
|
||||
require(list(screen).children().size() == 1, "Counter fixture must be reachable by identifier");
|
||||
String narration = list(screen).children().getFirst().getNarration().getString();
|
||||
require(narration.contains(Long.toString(Long.MAX_VALUE))
|
||||
&& narration.contains(Long.toString(Long.MAX_VALUE - 1))
|
||||
&& narration.contains(Long.toString(Long.MAX_VALUE - 2))
|
||||
&& narration.contains(Integer.toString(Integer.MAX_VALUE)),
|
||||
"Exact counters must remain available in narration without overflow or truncation");
|
||||
});
|
||||
context.waitTick();
|
||||
context.takeScreenshot("sanctuary-blocodex-fixture-large-counters");
|
||||
|
||||
context.clickScreenButton("sanctuary.blocodex.help");
|
||||
context.runOnClient(client -> {
|
||||
require(client.gui.screen() != screen, "Help must open an actual readable screen");
|
||||
assertWidgetBounds(client.gui.screen());
|
||||
});
|
||||
context.waitTick();
|
||||
context.takeScreenshot("sanctuary-blocodex-help-minimum-gui");
|
||||
context.clickScreenButton("gui.back");
|
||||
context.runOnClient(client -> {
|
||||
require(client.gui.screen() == screen, "Closing help must return to the same Blocodex");
|
||||
require(search(screen).getValue().equals(LARGE_COUNTER_ID), "Help must retain the search");
|
||||
verifyFragmentFailures(screen, fixture);
|
||||
});
|
||||
|
||||
Path diagnostics = Path.of("diagnostics").toAbsolutePath();
|
||||
Files.createDirectories(diagnostics);
|
||||
Files.writeString(diagnostics.resolve("sanctuary-blocodex-client.json"),
|
||||
new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"fixture", "Explicit client test snapshot; no server or saved world is used",
|
||||
"entries", fixture.size(), "fragments", partCount(fixture),
|
||||
"search_filters_pagination", true, "exact_counter_narration", true,
|
||||
"fragment_atomicity_and_stale_requests", true, "invalid_snapshots_clear_state", true,
|
||||
"screenshots", screenshots, "passed", true)) + "\n");
|
||||
SanctuaryMod.LOGGER.info("Blocodex native client checks passed: explicit fixture, real widgets, two GUI scales, exact counters and fragment atomicity.");
|
||||
} catch (IOException failure) {
|
||||
throw new AssertionError("Unable to write Blocodex client diagnostics", failure);
|
||||
} finally {
|
||||
context.runOnClient(client -> {
|
||||
client.gui.setScreen(previous);
|
||||
client.options.guiScale().set(previousScale);
|
||||
client.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BlockKnowledge> fixture() {
|
||||
List<BlockKnowledge> entries = new ArrayList<>();
|
||||
entries.add(new BlockKnowledge(LARGE_COUNTER_ID, true, true,
|
||||
Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2, Integer.MAX_VALUE));
|
||||
List<String> ids = BuiltInRegistries.BLOCK.stream()
|
||||
.filter(block -> block.asItem() != Items.AIR)
|
||||
.map(block -> BuiltInRegistries.BLOCK.getKey(block).toString())
|
||||
.filter(id -> !id.equals(LARGE_COUNTER_ID)).sorted().limit(258).toList();
|
||||
require(ids.size() == 258, "The native registry must contain enough blocks to exercise three fragments");
|
||||
for (int index = 0; index < ids.size(); index++) {
|
||||
int evidence = index % 7;
|
||||
entries.add(new BlockKnowledge(ids.get(index), evidence == 0, evidence == 1,
|
||||
evidence == 2 ? 7 : 0, evidence == 3 ? 11 : 0,
|
||||
evidence == 4 ? 13 : 0, evidence == 5 ? 17 : 0));
|
||||
}
|
||||
entries.add(new BlockKnowledge(REMOVED_MOD_ID, false, true, 0, 0, 0, 0));
|
||||
return List.copyOf(entries);
|
||||
}
|
||||
|
||||
private static void verifyControls(SanctuaryBlocodexScreen screen, List<BlockKnowledge> fixture) {
|
||||
search(screen).setValue(REMOVED_MOD_ID.toUpperCase(java.util.Locale.ROOT));
|
||||
require(filteredCount(screen) == 1 && list(screen).children().size() == 1,
|
||||
"Identifier search must ignore case and preserve absent mod identifiers");
|
||||
search(screen).setValue("fixture_no_matching_block");
|
||||
require(filteredCount(screen) == 0 && list(screen).children().isEmpty()
|
||||
&& !button(screen, "<").active && !button(screen, ">").active,
|
||||
"Empty search results must not retain a previous page or active pagination");
|
||||
search(screen).setValue("");
|
||||
for (var filter : Map.<String, Predicate<BlockKnowledge>>of(
|
||||
"UNKNOWN", entry -> !entry.known(), "KNOWN", BlockKnowledge::known,
|
||||
"SEEN", BlockKnowledge::seen, "POSSESSED", BlockKnowledge::possessed,
|
||||
"MINED", entry -> entry.mined() > 0, "DROPPED", entry -> entry.dropped() > 0,
|
||||
"PLACED", entry -> entry.placed() > 0, "CARRIED", entry -> entry.carried() > 0).entrySet()) {
|
||||
chooseFilter(screen, filter.getKey());
|
||||
require(filteredCount(screen) == fixture.stream().filter(filter.getValue()).count(),
|
||||
"Facet " + filter.getKey() + " must use only its own evidence");
|
||||
}
|
||||
chooseFilter(screen, "ALL");
|
||||
require(filteredCount(screen) == fixture.size(), "All filter must restore the whole catalogue");
|
||||
List<String> first = narrations(screen);
|
||||
require(!first.isEmpty() && button(screen, ">").active && !button(screen, "<").active,
|
||||
"A full catalogue must expose another page while starting on page one");
|
||||
button(screen, ">").onPress(PRESS);
|
||||
require(!narrations(screen).equals(first) && button(screen, "<").active,
|
||||
"Next page must replace the visible rows and enable return");
|
||||
button(screen, "<").onPress(PRESS);
|
||||
require(narrations(screen).equals(first), "Returning to the first page must restore its exact rows");
|
||||
}
|
||||
|
||||
private static void verifyFragmentFailures(SanctuaryBlocodexScreen screen, List<BlockKnowledge> fixture) {
|
||||
search(screen).setValue("");
|
||||
List<String> before = narrations(screen);
|
||||
fixtureRequest(screen, 402);
|
||||
screen.accept(part(fixture, 402, 0));
|
||||
screen.accept(part(fixture, 401, 1));
|
||||
require(narrations(screen).equals(before) && filteredCount(screen) == fixture.size(),
|
||||
"An incomplete refresh and stale fragment must retain only the prior complete snapshot");
|
||||
screen.accept(part(fixture, 402, 2));
|
||||
require(list(screen).children().isEmpty() && filteredCount(screen) == 0,
|
||||
"Out-of-order fragments must invalidate the view without inventing zero counters");
|
||||
|
||||
fixtureRequest(screen, 403);
|
||||
List<BlockKnowledge> duplicate = new ArrayList<>(fixture);
|
||||
duplicate.set(128, duplicate.getFirst());
|
||||
screen.accept(part(duplicate, 403, 0));
|
||||
screen.accept(part(duplicate, 403, 1));
|
||||
require(list(screen).children().isEmpty(), "Duplicate block identifiers across fragments must never publish");
|
||||
|
||||
fixtureRequest(screen, 404);
|
||||
screen.accept(new BlockKnowledgePayloads.SnapshotPart(404, 0, 1, 0, false, List.of()));
|
||||
require(list(screen).children().isEmpty() && !(boolean) read(screen, "available"),
|
||||
"An unhealthy server snapshot must remain unavailable rather than become an unknown catalogue");
|
||||
fixtureRequest(screen, 405);
|
||||
for (int index = 0; index < partCount(fixture); index++) screen.accept(part(fixture, 405, index));
|
||||
require(filteredCount(screen) == fixture.size(), "A complete fresh request must recover after a failed snapshot");
|
||||
}
|
||||
|
||||
private static BlockKnowledgePayloads.SnapshotPart part(List<BlockKnowledge> entries, int request, int index) {
|
||||
int start = index * BlockKnowledgePayloads.FRAGMENT_SIZE;
|
||||
return new BlockKnowledgePayloads.SnapshotPart(request, index, partCount(entries), entries.size(), true,
|
||||
entries.subList(start, Math.min(entries.size(), start + BlockKnowledgePayloads.FRAGMENT_SIZE)));
|
||||
}
|
||||
|
||||
private static int partCount(List<BlockKnowledge> entries) {
|
||||
return Math.max(1, (entries.size() + BlockKnowledgePayloads.FRAGMENT_SIZE - 1) / BlockKnowledgePayloads.FRAGMENT_SIZE);
|
||||
}
|
||||
|
||||
private static BlockKnowledgePayloads.SnapshotPart wireRoundTrip(BlockKnowledgePayloads.SnapshotPart part) {
|
||||
RegistryFriendlyByteBuf buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), RegistryAccess.EMPTY);
|
||||
try {
|
||||
BlockKnowledgePayloads.SnapshotPart.CODEC.encode(buffer, part);
|
||||
var decoded = BlockKnowledgePayloads.SnapshotPart.CODEC.decode(buffer);
|
||||
require(decoded.equals(part) && buffer.readableBytes() == 0,
|
||||
"The real payload codec must retain exact counters, flags and fragment boundaries");
|
||||
return decoded;
|
||||
} finally {
|
||||
buffer.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void fixtureRequest(SanctuaryBlocodexScreen screen, int request) {
|
||||
// Only tests bypass the live connection prerequisite. Production has no fixture entry point.
|
||||
write(screen, "activeRequest", request);
|
||||
write(screen, "nextPart", 0);
|
||||
write(screen, "expectedParts", -1);
|
||||
write(screen, "expectedTotal", -1);
|
||||
write(screen, "pending", true);
|
||||
write(screen, "lastRequestTick", 1_000_000); // Keep screenshots independent of offline auto-refresh.
|
||||
((List<?>) read(screen, "incoming")).clear();
|
||||
((java.util.Set<?>) read(screen, "incomingIds")).clear();
|
||||
}
|
||||
|
||||
private static int filteredCount(SanctuaryBlocodexScreen screen) {
|
||||
return ((List<?>) read(screen, "filtered")).size();
|
||||
}
|
||||
|
||||
private static List<String> narrations(SanctuaryBlocodexScreen screen) {
|
||||
return list(screen).children().stream().map(entry -> entry.getNarration().getString()).toList();
|
||||
}
|
||||
|
||||
private static ObjectSelectionList<?> list(Screen screen) {
|
||||
return screen.children().stream().filter(ObjectSelectionList.class::isInstance)
|
||||
.map(child -> (ObjectSelectionList<?>) child).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static EditBox search(Screen screen) {
|
||||
return screen.children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static Button button(Screen screen, String text) {
|
||||
return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(button -> button.getMessage().getString().equals(text)).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static void chooseFilter(Screen screen, String target) {
|
||||
CycleButton<?> button = screen.children().stream().filter(CycleButton.class::isInstance)
|
||||
.map(CycleButton.class::cast).findFirst().orElseThrow();
|
||||
int attempts = 9;
|
||||
while (!button.getValue().toString().equals(target) && attempts-- > 0) button.onPress(PRESS);
|
||||
require(button.getValue().toString().equals(target), "Filter cycle must expose " + target);
|
||||
}
|
||||
|
||||
private static void assertWidgetBounds(Screen screen) {
|
||||
for (var child : screen.children()) {
|
||||
if (child instanceof AbstractWidget widget) {
|
||||
require(widget.getX() >= 0 && widget.getY() >= 0
|
||||
&& widget.getX() + widget.getWidth() <= screen.width
|
||||
&& widget.getY() + widget.getHeight() <= screen.height,
|
||||
"Widget must remain inside the native GUI: " + widget.getMessage().getString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object read(Object target, String name) {
|
||||
try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(target); }
|
||||
catch (ReflectiveOperationException failure) { throw new AssertionError("Missing client fixture field " + name, failure); }
|
||||
}
|
||||
|
||||
private static void write(Object target, String name, Object value) {
|
||||
try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); field.set(target, value); }
|
||||
catch (ReflectiveOperationException failure) { throw new AssertionError("Missing client fixture field " + name, failure); }
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.client.SanctuaryBlocodexScreen;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledge;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgePayloads;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeService;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.ObjectSelectionList;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real key mapping, connection and server observations in a newly created development world. */
|
||||
final class BlocodexNetworkClientChecks {
|
||||
private static final String SEEN_ID = "minecraft:gold_block";
|
||||
private static final String POSSESSED_ID = "minecraft:diamond_block";
|
||||
private static final String UNKNOWN_ID = "minecraft:emerald_block";
|
||||
|
||||
static void run(ClientGameTestContext context) {
|
||||
Path runDirectory = Path.of("").toAbsolutePath().normalize();
|
||||
Path saves = context.computeOnClient(client -> client.getLevelSource().getBaseDir().toAbsolutePath().normalize());
|
||||
require(runDirectory.endsWith(Path.of("build", "run", "clientGameTest"))
|
||||
&& saves.startsWith(runDirectory),
|
||||
"The network test must create worlds only inside the ignored development client run directory");
|
||||
context.runOnClient(client -> {
|
||||
var matches = Arrays.stream(client.options.keyMappings)
|
||||
.filter(key -> key.getName().equals("sanctuary.blocodex.key")).toList();
|
||||
require(matches.size() == 1, "Exactly one innate Blocodex key mapping must be registered");
|
||||
var key = matches.getFirst();
|
||||
require(key.getCategory().id().equals(SanctuaryMod.id("sanctuary"))
|
||||
&& key.matches(new KeyEvent(InputConstants.KEY_B, 0, 0)),
|
||||
"The Sanctuary category must expose the real B binding in the development client");
|
||||
});
|
||||
|
||||
String worldName = "Sanctuary Blocodex network " + UUID.randomUUID();
|
||||
Path saveDirectory;
|
||||
UUID playerId;
|
||||
Map<String, BlockKnowledge> firstServerSnapshot;
|
||||
Map<String, BlockKnowledge> secondServerSnapshot;
|
||||
// Fabric's consistent defaults are a fresh flat world, seed 1, no structures or mobs.
|
||||
try (var world = context.worldBuilder().adjustSettings(settings -> {
|
||||
settings.setName(worldName);
|
||||
settings.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
settings.setAllowCommands(false);
|
||||
}).create()) {
|
||||
saveDirectory = world.getWorldSave().getSaveDirectory().toAbsolutePath().normalize();
|
||||
require(saveDirectory.startsWith(saves) && !saveDirectory.equals(saves),
|
||||
"The new test save must remain inside the development saves directory");
|
||||
var connection = world.getConnection();
|
||||
var serverContext = world.getServer();
|
||||
connection.waitForChunksDownload(1200);
|
||||
context.waitFor(client -> client.player != null && client.gui.screen() == null
|
||||
&& ClientPlayNetworking.canSend(BlockKnowledgePayloads.Request.TYPE), 600);
|
||||
playerId = serverContext.computeOnServer(server -> player(server).getUUID());
|
||||
serverContext.runOnServer(server -> {
|
||||
require(!BlockKnowledgeService.knowledge(player(server), Blocks.GOLD_BLOCK).known(),
|
||||
"A fresh flat world must not already know the observation target");
|
||||
require(!BlockKnowledgeService.knowledge(player(server), Blocks.DIAMOND_BLOCK).known(),
|
||||
"A fresh player must not already possess the inventory target");
|
||||
});
|
||||
|
||||
BlockPos target = serverContext.computeOnServer(server -> {
|
||||
ServerPlayer player = player(server);
|
||||
BlockPos pos = player.blockPosition().offset(0, 1, 3);
|
||||
// Explicit test setup; normal tick observation must discover both facts itself.
|
||||
player.level().setBlockAndUpdate(pos, Blocks.GOLD_BLOCK.defaultBlockState());
|
||||
player.getInventory().setItem(0, new ItemStack(Items.DIAMOND_BLOCK, 3));
|
||||
player.inventoryMenu.broadcastChanges();
|
||||
return pos;
|
||||
});
|
||||
connection.waitForClientboundPackets();
|
||||
context.waitFor(client -> client.level.getBlockState(target).is(Blocks.GOLD_BLOCK)
|
||||
&& client.player.getInventory().getItem(0).is(Items.DIAMOND_BLOCK), 600);
|
||||
context.getInput().lookAt(target);
|
||||
connection.waitForServerboundPackets();
|
||||
context.waitTicks(BlockKnowledgeService.OBSERVATION_INTERVAL + 1);
|
||||
serverContext.waitFor(server -> {
|
||||
ServerPlayer player = player(server);
|
||||
var observed = BlockKnowledgeService.knowledge(player, Blocks.GOLD_BLOCK);
|
||||
var carried = BlockKnowledgeService.knowledge(player, Blocks.DIAMOND_BLOCK);
|
||||
return observed.seen() && carried.possessed() && carried.carried() == 3;
|
||||
}, 600);
|
||||
firstServerSnapshot = expected(serverContext);
|
||||
require(firstServerSnapshot.get(SEEN_ID).seen()
|
||||
&& !firstServerSnapshot.get(SEEN_ID).possessed()
|
||||
&& !firstServerSnapshot.get(POSSESSED_ID).seen()
|
||||
&& firstServerSnapshot.get(POSSESSED_ID).carried() == 3
|
||||
&& !firstServerSnapshot.get(UNKNOWN_ID).known(),
|
||||
"The live server must retain independent seen, possessed and unknown states");
|
||||
|
||||
SanctuaryBlocodexScreen first = openWithB(context);
|
||||
context.runOnClient(client -> assertSnapshot(first, firstServerSnapshot));
|
||||
context.runOnClient(client -> search(first).setValue(POSSESSED_ID));
|
||||
context.waitTick();
|
||||
context.takeScreenshot("sanctuary-blocodex-network-carried-three");
|
||||
context.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
context.waitFor(client -> client.gui.screen() == null, 120);
|
||||
|
||||
serverContext.runOnServer(server -> {
|
||||
ServerPlayer player = player(server);
|
||||
player.getInventory().setItem(0, ItemStack.EMPTY);
|
||||
player.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
context.waitTicks(2);
|
||||
serverContext.waitFor(server -> {
|
||||
var knowledge = BlockKnowledgeService.knowledge(player(server), Blocks.DIAMOND_BLOCK);
|
||||
return knowledge.possessed() && knowledge.carried() == 0;
|
||||
}, 600);
|
||||
connection.waitForClientboundPackets();
|
||||
secondServerSnapshot = expected(serverContext);
|
||||
|
||||
// The production request limiter uses wall time, while game-test ticks can run faster.
|
||||
long reopenAt = System.nanoTime() + 1_100_000_000L;
|
||||
context.waitFor(client -> System.nanoTime() >= reopenAt, 2400);
|
||||
SanctuaryBlocodexScreen second = openWithB(context);
|
||||
require(second != first, "Closing and reopening with B must issue a fresh screen request");
|
||||
context.runOnClient(client -> {
|
||||
assertSnapshot(second, secondServerSnapshot);
|
||||
search(second).setValue(POSSESSED_ID);
|
||||
require(list(second).children().size() == 1,
|
||||
"The received block must remain searchable after its current inventory becomes empty");
|
||||
});
|
||||
context.waitTick();
|
||||
context.takeScreenshot("sanctuary-blocodex-network-sticky-possession");
|
||||
context.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
context.waitFor(client -> client.gui.screen() == null, 120);
|
||||
}
|
||||
|
||||
try {
|
||||
Path sidecar = saveDirectory.resolve("data/sanctuary-blocodex").resolve(playerId + ".json");
|
||||
require(Files.isRegularFile(sidecar), "Closing the real server must persist its observed player history");
|
||||
Path diagnostics = runDirectory.resolve("diagnostics");
|
||||
Files.createDirectories(diagnostics);
|
||||
Files.writeString(diagnostics.resolve("sanctuary-blocodex-network-client.json"),
|
||||
new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"fixture", "New flat development world; gold block and three diamond blocks prepared on the server",
|
||||
"save_directory", saveDirectory.toString(), "player_uuid", playerId.toString(),
|
||||
"real_b_key_and_payloads", true, "server_gaze_and_inventory", true,
|
||||
"before_removal", firstServerSnapshot, "after_removal", secondServerSnapshot,
|
||||
"sidecar_saved_on_close", true, "passed", true)) + "\n");
|
||||
SanctuaryMod.LOGGER.info("Blocodex network client checks passed: real B key, native server snapshots, observed gaze, sticky possession and empty carried stock in {}", saveDirectory);
|
||||
} catch (IOException failure) {
|
||||
throw new AssertionError("Unable to verify native Blocodex network diagnostics", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static SanctuaryBlocodexScreen openWithB(ClientGameTestContext context) {
|
||||
context.getInput().pressKey(InputConstants.KEY_B);
|
||||
context.waitForScreen(SanctuaryBlocodexScreen.class);
|
||||
context.waitFor(client -> client.gui.screen() instanceof SanctuaryBlocodexScreen screen
|
||||
&& (boolean) read(screen, "loaded"), 1200);
|
||||
return context.computeOnClient(client -> (SanctuaryBlocodexScreen) client.gui.screen());
|
||||
}
|
||||
|
||||
private static Map<String, BlockKnowledge> expected(TestServerContext context) {
|
||||
return context.computeOnServer(server -> {
|
||||
ServerPlayer player = player(server);
|
||||
return Map.of(SEEN_ID, BlockKnowledgeService.knowledge(player, Blocks.GOLD_BLOCK),
|
||||
POSSESSED_ID, BlockKnowledgeService.knowledge(player, Blocks.DIAMOND_BLOCK),
|
||||
UNKNOWN_ID, BlockKnowledgeService.knowledge(player, Blocks.EMERALD_BLOCK));
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertSnapshot(SanctuaryBlocodexScreen screen, Map<String, BlockKnowledge> expected) {
|
||||
require((boolean) read(screen, "available"), "The real server snapshot must be available");
|
||||
List<?> rows = (List<?>) read(screen, "catalogue");
|
||||
Map<String, BlockKnowledge> received = rows.stream().map(BlocodexNetworkClientChecks::knowledge)
|
||||
.collect(Collectors.toUnmodifiableMap(BlockKnowledge::blockId, value -> value));
|
||||
for (var entry : expected.entrySet()) {
|
||||
require(entry.getValue().equals(received.get(entry.getKey())),
|
||||
"The live network snapshot must retain the server's exact evidence and counters for " + entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockKnowledge knowledge(Object row) {
|
||||
try {
|
||||
Method method = row.getClass().getDeclaredMethod("knowledge");
|
||||
method.setAccessible(true);
|
||||
return (BlockKnowledge) method.invoke(row);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError("Unable to read received Blocodex row", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerPlayer player(MinecraftServer server) {
|
||||
List<ServerPlayer> players = server.getPlayerList().getPlayers();
|
||||
require(players.size() == 1, "The development integrated server must have one actual connected player");
|
||||
return players.getFirst();
|
||||
}
|
||||
|
||||
private static EditBox search(SanctuaryBlocodexScreen screen) {
|
||||
return screen.children().stream().filter(EditBox.class::isInstance)
|
||||
.map(EditBox.class::cast).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static ObjectSelectionList<?> list(SanctuaryBlocodexScreen screen) {
|
||||
return screen.children().stream().filter(ObjectSelectionList.class::isInstance)
|
||||
.map(child -> (ObjectSelectionList<?>) child).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static Object read(Object target, String name) {
|
||||
try { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(target); }
|
||||
catch (ReflectiveOperationException failure) { throw new AssertionError("Unable to read received Blocodex state " + name, failure); }
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
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 render, UI and network checks in fresh ignored development worlds. */
|
||||
public final class SanctuaryClientRenderTests implements FabricClientGameTest {
|
||||
@Override
|
||||
public void runTest(ClientGameTestContext context) {
|
||||
context.runOnClient(client -> {
|
||||
assertApplied(client.getClass().getClassLoader(),
|
||||
"net.minecraft.client.renderer.SkyRenderer", "sanctuary$openLowerSky");
|
||||
assertApplied(client.getClass().getClassLoader(),
|
||||
"net.minecraft.client.renderer.fog.FogRenderer", "sanctuary$openVoidFog");
|
||||
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);
|
||||
}
|
||||
|
||||
private static void assertApplied(ClassLoader loader, String target, String hook) {
|
||||
try {
|
||||
// Force transformation even when this renderer is lazily loaded on world entry.
|
||||
Class<?> transformed = Class.forName(target, true, loader);
|
||||
boolean applied = Arrays.stream(transformed.getDeclaredMethods())
|
||||
.anyMatch(method -> method.getName().contains(hook));
|
||||
if (!applied) {
|
||||
throw new AssertionError("Missing Sanctuary render injection: " + target + " / " + hook);
|
||||
}
|
||||
} catch (ClassNotFoundException failure) {
|
||||
throw new AssertionError("Minecraft render target is missing: " + target, failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.client.SanctuaryWorldOptionsScreen;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.CycleButton;
|
||||
import net.minecraft.client.gui.components.tabs.TabNavigationBar;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.WorldGenSettings;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
|
||||
/** The real new-world button path and saved settings, without starting or opening a game world. */
|
||||
final class SanctuaryWorldOptionsClientChecks {
|
||||
private static final KeyEvent PRESS = new KeyEvent(InputConstants.KEY_RETURN, 0, 0);
|
||||
|
||||
static void run(ClientGameTestContext context) {
|
||||
Screen previous = context.computeOnClient(client -> client.gui.screen());
|
||||
context.runOnClient(client -> CreateWorldScreen.openFresh(client, () -> client.gui.setScreen(previous)));
|
||||
context.waitFor(client -> client.gui.screen() instanceof CreateWorldScreen, 1200);
|
||||
CreateWorldScreen create = context.computeOnClient(client -> (CreateWorldScreen) client.gui.screen());
|
||||
List<Map<String, Object>> savedSizes = new ArrayList<>();
|
||||
try {
|
||||
context.runOnClient(client -> {
|
||||
TabNavigationBar tabs = create.children().stream().filter(TabNavigationBar.class::isInstance)
|
||||
.map(TabNavigationBar.class::cast).findFirst().orElseThrow();
|
||||
tabs.selectTab(1, false); // The vanilla World tab owns the world type and Customize widgets.
|
||||
var state = create.getUiState();
|
||||
long sanctuaryChoices = state.getNormalPresetList().stream().filter(entry -> entry.preset() != null
|
||||
&& entry.preset().getRegisteredName().startsWith("sanctuary:")).count();
|
||||
require(sanctuaryChoices == 1, "Exactly one public Sanctuary choice must remain");
|
||||
require(state.getAltPresetList().stream().filter(entry -> entry.preset() != null
|
||||
&& entry.preset().getRegisteredName().startsWith("sanctuary:")).count() == 1,
|
||||
"The extended world-type cycle must not expose the old laboratory or sizes");
|
||||
CycleButton<?> worldType = create.children().stream().filter(CycleButton.class::isInstance)
|
||||
.map(CycleButton.class::cast)
|
||||
.filter(button -> button.getValue() instanceof WorldCreationUiState.WorldTypeEntry)
|
||||
.findFirst().orElseThrow();
|
||||
int attempts = state.getNormalPresetList().size();
|
||||
while (!isSanctuary(state) && attempts-- > 0) worldType.onPress(PRESS);
|
||||
require(isSanctuary(state), "The real world-type cycle must reach Sanctuary");
|
||||
require(diameter(create) == 724, "Sanctuary defaults to Medium");
|
||||
state.setSeed("42");
|
||||
state.setGenerateStructures(false);
|
||||
state.setBonusChest(true);
|
||||
});
|
||||
|
||||
// Cancel and Escape both leave the pending generator untouched.
|
||||
context.clickScreenButton("selectWorld.customizeType");
|
||||
context.waitForScreen(SanctuaryWorldOptionsScreen.class);
|
||||
context.runOnClient(client -> chooseSize(client.gui.screen(), "LARGE"));
|
||||
context.clickScreenButton("gui.cancel");
|
||||
context.runOnClient(client -> require(diameter(create) == 724, "Cancel must not apply Large"));
|
||||
context.clickScreenButton("selectWorld.customizeType");
|
||||
context.runOnClient(client -> {
|
||||
chooseSize(client.gui.screen(), "SMALL");
|
||||
client.gui.screen().keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE, 0, 0));
|
||||
require(client.gui.screen() == create && diameter(create) == 724,
|
||||
"Escape must return to creation without applying Small");
|
||||
});
|
||||
|
||||
Path diagnostics = Path.of("diagnostics").toAbsolutePath();
|
||||
Files.createDirectories(diagnostics);
|
||||
Path testRoot = Files.createTempDirectory(diagnostics, "world-options-settings-");
|
||||
var storage = LevelStorageSource.createDefault(testRoot);
|
||||
for (var size : Map.of("SMALL", 512, "MEDIUM", 724, "LARGE", 1024).entrySet()) {
|
||||
var before = context.computeOnClient(client -> create.getUiState().getSettings());
|
||||
context.clickScreenButton("selectWorld.customizeType");
|
||||
context.waitForScreen(SanctuaryWorldOptionsScreen.class);
|
||||
context.runOnClient(client -> chooseSize(client.gui.screen(), size.getKey()));
|
||||
context.clickScreenButton("gui.done");
|
||||
context.runOnClient(client -> {
|
||||
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");
|
||||
}
|
||||
require(isSanctuary(create.getUiState()), "Customization must keep the single Sanctuary world type");
|
||||
create.getUiState().setSettings(after.withOptions(options -> options));
|
||||
require(diameter(create) == size.getValue(),
|
||||
"Refreshing world settings must preserve the custom size instead of restoring the preset default");
|
||||
String name = size.getKey().toLowerCase(java.util.Locale.ROOT);
|
||||
var settings = new WorldGenSettings(after.options(), after.selectedDimensions());
|
||||
try (var save = storage.createAccess(name)) {
|
||||
LevelStorageSource.writeWorldGenSettings(after.worldgenLoadContext(), storage.getLevelPath(name), settings);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, after.worldgenLoadContext(),
|
||||
WorldGenSettings.TYPE).getOrThrow();
|
||||
require(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator generator
|
||||
&& generator.initialDiameter() == size.getValue(), "Selected size must survive actual settings storage");
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, after.worldgenLoadContext());
|
||||
require(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
|
||||
.equals(WorldGenSettings.CODEC.encodeStart(ops, settings).getOrThrow()),
|
||||
"Stored options and dimensions must remain unchanged");
|
||||
savedSizes.add(Map.of("size", name, "diameter", size.getValue(), "disk_round_trip", true));
|
||||
}
|
||||
});
|
||||
context.clickScreenButton("selectWorld.customizeType");
|
||||
context.runOnClient(client -> require(sizeButton(client.gui.screen()).getValue().toString().equals(size.getKey()),
|
||||
"Reopening Customize must restore the saved selection"));
|
||||
if (size.getKey().equals("MEDIUM")) context.takeScreenshot("sanctuary-world-options-medium");
|
||||
context.clickScreenButton("gui.cancel");
|
||||
}
|
||||
|
||||
context.runOnClient(client -> {
|
||||
var state = create.getUiState();
|
||||
var settings = state.getSettings();
|
||||
var presets = settings.worldgenLoadContext().lookupOrThrow(Registries.WORLD_PRESET);
|
||||
var legacy = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_5")))
|
||||
.value().createWorldDimensions().overworld();
|
||||
state.updateDimensions((registries, dimensions) -> dimensions.replaceOverworldGenerator(registries, legacy));
|
||||
require(state.getPresetEditor() == null,
|
||||
"A legacy generator under the public key must not gain an implicit conversion editor");
|
||||
state.setSettings(settings);
|
||||
var flat = presets.getOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT);
|
||||
state.setWorldType(new WorldCreationUiState.WorldTypeEntry(flat));
|
||||
require(state.getPresetEditor() != null
|
||||
&& !(state.getPresetEditor().createEditScreen(create, state.getSettings()) instanceof SanctuaryWorldOptionsScreen),
|
||||
"Vanilla flat-world customization must remain available");
|
||||
});
|
||||
Files.writeString(diagnostics.resolve("sanctuary-world-options-client.json"),
|
||||
new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"public_sanctuary_choices", 1, "default_diameter", 724,
|
||||
"saved_sizes", savedSizes, "cancel_and_escape_preserved", true,
|
||||
"settings_refresh_preserved", true,
|
||||
"legacy_editor_guard", true, "vanilla_editor_preserved", true,
|
||||
"test_settings_directory", testRoot.toString(), "passed", true)) + "\n");
|
||||
SanctuaryMod.LOGGER.info("Sanctuary world options client tests passed: one world type, three saved sizes, Done/Cancel/Escape.");
|
||||
} catch (IOException failure) {
|
||||
throw new AssertionError("Unable to verify the selected world settings on disk", failure);
|
||||
} finally {
|
||||
context.runOnClient(client -> create.onClose());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSanctuary(WorldCreationUiState state) {
|
||||
var preset = state.getWorldType().preset();
|
||||
return preset != null && preset.getRegisteredName().equals("sanctuary:sanctuary");
|
||||
}
|
||||
|
||||
private static int diameter(CreateWorldScreen create) {
|
||||
return ((SanctuaryChunkGenerator) create.getUiState().getSettings().selectedDimensions().overworld()).initialDiameter();
|
||||
}
|
||||
|
||||
private static CycleButton<?> sizeButton(Screen screen) {
|
||||
require(screen instanceof SanctuaryWorldOptionsScreen, "Customize must open the Sanctuary screen");
|
||||
return screen.children().stream().filter(CycleButton.class::isInstance).map(CycleButton.class::cast)
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private static void chooseSize(Screen screen, String target) {
|
||||
CycleButton<?> button = sizeButton(screen);
|
||||
int attempts = 3;
|
||||
while (!button.getValue().toString().equals(target) && attempts-- > 0) button.onPress(PRESS);
|
||||
require(button.getValue().toString().equals(target), "Size cycle must expose " + target);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.*;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.aerial.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
|
||||
/** Separate disposable world: real admission before visiting satellites, then an actual ticking waterfall. */
|
||||
public final class Aerial24FlowAdmissionGameTests {
|
||||
private record Request(String id, String direction, int diameter, ExpansionIsland raw, ExpansionIsland filtered) {}
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void reservedSatellitesAndNativeWaterfall(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 24 && generator.experimentalStructures()
|
||||
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
|
||||
var plan = generator.aerialPlan24();
|
||||
var session = Objects.requireNonNull(ExpansionRuntime.session(level));
|
||||
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 4, "Use an untouched disposable world for admission witnesses");
|
||||
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
|
||||
String dimension = level.dimension().identifier().toString();
|
||||
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
|
||||
helper.assertTrue((evidence.chunks().getOrDefault(new StructureCensusMemory.ChunkKey(dimension, chunk.x(), chunk.z()), 0) & StructureCensusMemory.FULL) == 0,
|
||||
"Satellite claims are protected before any of their chunks reach FULL");
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter()); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("unvisited_claims_verified", true); report.put("sites", plan.sites());
|
||||
var admitted = new ArrayList<ExpansionIsland>();
|
||||
try {
|
||||
report.put("stage", "real admission toward an unseen satellite");
|
||||
if (!requests(level.getSeed(), session.islands(), plan, true).isEmpty())
|
||||
admitted.add(admit(helper, level, generator, session, plan, true, report));
|
||||
else report.put("collision_admission", "No raw expansion candidate reaches the close sites; initial island ownership already excludes them");
|
||||
report.put("stage", "real admission through an unclaimed interval");
|
||||
admitted.add(admit(helper, level, generator, session, plan, false, report));
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
var waterfall = plan.sites().stream().filter(AerialPlan24.Site::waterfall).findFirst().orElseThrow();
|
||||
var requested = waterfall.claimedChunks(); var iterator = requested.iterator();
|
||||
boolean[] tickets = {false}; int[] flowTicks = {0};
|
||||
Runnable release = () -> {
|
||||
if (tickets[0]) requested.forEach(chunk -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, chunk, 2));
|
||||
tickets[0] = false;
|
||||
};
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (admitted.stream().anyMatch(island -> !session.island(island.id()).status().equals("ready"))) {
|
||||
report.put("stage", "prepare admitted expansions"); report.put("progress", session.progress());
|
||||
helper.assertTrue(!session.progress().contains("failed") && !session.progress().contains("reload_required")
|
||||
&& !session.progress().contains("unavailable"), "Real admission preparation progresses without reload or failure");
|
||||
helper.runAtTickTime(helper.getTick() + 5, () -> step[0].run()); return;
|
||||
}
|
||||
if (!report.containsKey("native_ready_starts")) {
|
||||
var nativeStarts = new TreeMap<String, Integer>();
|
||||
for (var reserved : admitted) {
|
||||
var ready = session.island(reserved.id());
|
||||
helper.assertTrue(ready.equals(reserved.ready()) && !plan.conflicts(ready), "READY retains its immutable safe placement");
|
||||
int count = 0;
|
||||
for (var candidate : ExpansionStructures23.candidates(ready)) {
|
||||
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
count += (int) chunk.getAllStarts().values().stream().filter(StructureStart::isValid)
|
||||
.filter(start -> ((Object) start) instanceof NativeStructureMetadata23 metadata && metadata.sanctuary$native23() != null).count();
|
||||
}
|
||||
helper.assertTrue(count > 0, "Admitted generation24 expansion produces at least one real native start");
|
||||
nativeStarts.put(ready.id(), count);
|
||||
}
|
||||
report.put("native_ready_starts", nativeStarts);
|
||||
}
|
||||
if (iterator.hasNext()) {
|
||||
var chunk = iterator.next(); report.put("stage", "FULL waterfall " + chunk);
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!tickets[0]) {
|
||||
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
|
||||
}
|
||||
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks beneath the suspended island");
|
||||
var source = AerialGeometry24.waterfallSource(waterfall);
|
||||
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
|
||||
// Alpha28 islets stand above real terrain: a cascade may land on Sanctuary.
|
||||
// Require native flow through the free air gap, not an impossible route through rock.
|
||||
var below = waterAt(level, waterfall, waterfall.minY() - 1);
|
||||
var low = waterAt(level, waterfall, waterfall.minY() - AerialPlanner24.MIN_ISLET_GAP);
|
||||
report.put("water_below_islet", below == null ? List.of() : coords(below));
|
||||
report.put("water_across_air_gap", low == null ? List.of() : coords(low));
|
||||
if (below == null || low == null) {
|
||||
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall through the sixteen-block air gap");
|
||||
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
|
||||
}
|
||||
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
|
||||
report.put("expansions", admitted.stream().map(island -> session.island(island.id())).toList());
|
||||
report.put("passed", true);
|
||||
StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist the disposable flow/admission witness");
|
||||
write(report, "full.json"); release.run(); helper.succeed();
|
||||
} catch (Exception error) { release.run(); failure(report, error); throw new IllegalStateException("Aerial24 flow/admission failed: " + error.getMessage(), error); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
}
|
||||
|
||||
private static ExpansionIsland admit(GameTestHelper helper, ServerLevel level, SanctuaryChunkGenerator generator,
|
||||
ExpansionRuntime.Session session, AerialPlan24 plan, boolean collision, Map<String, Object> report) throws Exception {
|
||||
var requests = requests(level.getSeed(), session.islands(), plan, collision);
|
||||
helper.assertTrue(!requests.isEmpty(), "The finite candidate search contains a " + (collision ? "satellite overlap" : "free interval") + " witness");
|
||||
var attempts = new ArrayList<Map<String, Object>>(); report.put(collision ? "collision_attempts" : "interval_attempts", attempts);
|
||||
for (var request : requests.stream().limit(8).toList()) {
|
||||
var before = List.copyOf(session.islands());
|
||||
var filtered = session.candidate(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
|
||||
helper.assertTrue(filtered.equals(request.filtered()), "Real runtime candidate uses the same immutable aerial claims before any terrain inspection");
|
||||
if (collision) helper.assertTrue(plan.conflicts(request.raw()) && !plan.conflicts(filtered) && !filtered.equals(request.raw()),
|
||||
"The first overlapping candidate is rejected before the satellite is visited");
|
||||
long start = System.nanoTime();
|
||||
try {
|
||||
var reserved = session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural");
|
||||
long elapsed = (System.nanoTime() - start) / 1_000_000;
|
||||
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
|
||||
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
|
||||
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
|
||||
|
||||
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
|
||||
"Reservation retry is idempotent");
|
||||
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
|
||||
return reserved;
|
||||
} catch (IllegalArgumentException refused) {
|
||||
helper.assertTrue(before.equals(session.islands()), "Refused native admission writes no reservation");
|
||||
attempts.add(Map.of("request", request, "refused", String.valueOf(refused.getMessage()), "elapsed_ms", (System.nanoTime() - start) / 1_000_000));
|
||||
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No native expansion fits the bounded aerial admission witnesses");
|
||||
}
|
||||
|
||||
private static List<Request> requests(long seed, List<ExpansionIsland> islands, AerialPlan24 plan, boolean collision) {
|
||||
var result = new ArrayList<Request>();
|
||||
for (int index = 0; index < 32 && result.size() < 16; index++) for (var direction : ExpansionIsland.DIRECTIONS) {
|
||||
String id = (collision ? "aerial_collision_" : "aerial_interval_") + index + "_" + direction.replace("_", "");
|
||||
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
|
||||
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
|
||||
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
|
||||
if (filtered == null) continue;
|
||||
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static boolean inAerialBand(ExpansionIsland island, AerialPlan24 plan) {
|
||||
double outside = plan.claimsChunks().stream().map(ChunkPos::unpack).mapToDouble(chunk -> Math.hypot(chunk.getMiddleBlockX(), chunk.getMiddleBlockZ()) + 12).max().orElseThrow();
|
||||
return Math.hypot(island.centerX(), island.centerZ()) - island.reservedRadius() < outside;
|
||||
}
|
||||
private static BlockPos waterAt(ServerLevel level, AerialPlan24.Site site, int y) {
|
||||
for (int x = site.minX() - 3; x <= site.maxX() + 3; x++) for (int z = site.minZ() - 3; z <= site.maxZ() + 3; z++) {
|
||||
var pos = new BlockPos(x, y, z);
|
||||
if (level.getFluidState(pos).is(FluidTags.WATER)) return pos;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static void write(Map<String, Object> report, String name) throws java.io.IOException {
|
||||
var directory = Path.of("diagnostics/aerial24-flow-admission"); Files.createDirectories(directory);
|
||||
Files.writeString(directory.resolve(name), new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
}
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("failure", error.toString());
|
||||
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
|
||||
SanctuaryMod.LOGGER.error("Aerial24 flow/admission failed at {}", report.get("stage"), error);
|
||||
try { write(report, "failure.json"); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
|
||||
}
|
||||
}
|
||||
-459
@@ -1,459 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.aerial.*;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** One real world per size/seed. Separate JVM phases preserve looting, mining and extracted occupants. */
|
||||
public final class Aerial24WorldGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try { begin(helper, report); }
|
||||
catch (Exception failure) { failure(report, failure); throw failure; }
|
||||
}
|
||||
|
||||
private static void begin(GameTestHelper helper, Map<String, Object> report) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 24, "Aerial24 requires a disposable generation24 world");
|
||||
String phase = System.getProperty("sanctuary.test.aerial24.phase", "create");
|
||||
helper.assertTrue(List.of("create", "reload1", "reload2").contains(phase), "Known aerial lifecycle phase");
|
||||
boolean create = phase.equals("create");
|
||||
var plan = generator.aerialPlan24();
|
||||
report.put("phase", phase); report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("experimental", generator.experimentalStructures());
|
||||
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
if (!level.structureManager().shouldGenerateStructures() || !generator.experimentalStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.claimsChunks().isEmpty(), "Disabled aerial generation reserves no space");
|
||||
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
|
||||
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
|
||||
}
|
||||
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
|
||||
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
|
||||
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
|
||||
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
|
||||
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
|
||||
report.put("census_before", StructureCensusService.aerialSites(level));
|
||||
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
|
||||
var journalBefore = files(level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()));
|
||||
verifyClaims(helper, generator, plan, report);
|
||||
Path world = level.getServer().getWorldPath(LevelResource.ROOT);
|
||||
JsonObject previous = create ? null : JsonParser.parseString(Files.readString(world.resolve("aerial24-" + (phase.equals("reload1") ? "create" : "reload1") + ".json"))).getAsJsonObject();
|
||||
if (previous != null) {
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload occurs in a separate JVM after a successful phase");
|
||||
helper.assertTrue(previous.get("geometry").equals(JSON.toJsonTree(plan.sites())), "Site geometry and palette seeds survive reload");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(report.get("generator_codec")), "Generator options survive reload");
|
||||
}
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
plan.sites().forEach(site -> chunks.addAll(site.claimedChunks()));
|
||||
plan.sites().forEach(site -> chunks.add(ChunkPos.containing(new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ()))));
|
||||
// The two extracted merchants are retained in a separate, existing root chunk.
|
||||
chunks.add(new ChunkPos(0, 0));
|
||||
var loadOrder = new ArrayList<>(chunks);
|
||||
boolean reverseChunks = Boolean.getBoolean("sanctuary.test.aerial24.reverseChunks");
|
||||
if (reverseChunks) Collections.reverse(loadOrder);
|
||||
report.put("reverse_chunks", reverseChunks);
|
||||
var iterator = loadOrder.iterator();
|
||||
var entityChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().stream().filter(AerialPlan24.Site::merchant).forEach(site -> entityChunks.addAll(site.claimedChunks()));
|
||||
entityChunks.add(new ChunkPos(0, 0));
|
||||
boolean[] tickets = {false}; int[] wait = {0};
|
||||
Runnable release = () -> {
|
||||
if (tickets[0]) entityChunks.forEach(pos -> level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, pos, 2));
|
||||
tickets[0] = false;
|
||||
};
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var chunk = iterator.next(); report.put("stage", "FULL " + chunk);
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!report.containsKey("native_departures_ready")) {
|
||||
// Fail on a missing native start immediately instead of waiting for entities
|
||||
// which could never have been placed (for example, an obsolete void-only gate).
|
||||
report.put("stage", "verify saved aerial departures before entity loading");
|
||||
report.put("native_departures_ready", starts(helper, level, plan).size());
|
||||
}
|
||||
if (!tickets[0]) {
|
||||
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
|
||||
}
|
||||
var occupants = occupants(level, plan);
|
||||
var readiness = new TreeMap<String, Object>(); boolean entitiesReady = true;
|
||||
for (var pos : entityChunks) {
|
||||
boolean loaded = level.areEntitiesLoaded(pos.pack()), ticking = level.areEntitiesActuallyLoadedAndTicking(pos);
|
||||
readiness.put(pos.toString(), Map.of("loaded", loaded, "ticking", ticking));
|
||||
entitiesReady &= loaded && ticking;
|
||||
}
|
||||
report.put("entity_chunks", readiness);
|
||||
report.put("occupants_observed", occupants.stream().map(villager -> Map.of("uuid", villager.getUUID().toString(),
|
||||
"position", List.of(villager.getX(), villager.getY(), villager.getZ()))).toList());
|
||||
report.put("visible_occupants", occupants.size()); report.put("entity_wait_ticks", wait[0]);
|
||||
if ((!entitiesReady || occupants.size() < 2) && wait[0]++ < 600) {
|
||||
report.put("stage", "wait for persistent merchants");
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
helper.assertTrue(entitiesReady, "All ship chunks finish native entity loading and become ticking before occupant inspection");
|
||||
helper.assertTrue(occupants.size() == 2, "Both one-time merchants are present after native entity loading");
|
||||
report.put("stage", "inspect native structures");
|
||||
var starts = starts(helper, level, plan);
|
||||
var before = inspect(helper, level, plan, create, report);
|
||||
if (create) {
|
||||
verifyLocator(helper, level, plan, starts);
|
||||
report.put("stage", "loot, extract and dismantle disposable witnesses");
|
||||
report.put("mutations", mutate(helper, level, plan, occupants, before));
|
||||
} else {
|
||||
report.put("mutations", previous.get("mutations"));
|
||||
verifyMutations(helper, level, plan, previous.getAsJsonObject("mutations"), occupants);
|
||||
}
|
||||
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());
|
||||
report.put("blocks_after", countSites(level, plan));
|
||||
var census = StructureCensusService.aerialSites(level);
|
||||
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
|
||||
"Four historical generated sites remain indexed after loot, mining and demolition");
|
||||
report.put("census_after", census);
|
||||
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
|
||||
report.put("expansion_journal_unchanged", true);
|
||||
StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save actual blocks, loot state, starts and occupants");
|
||||
report.put("passed", true);
|
||||
write(world.resolve("aerial24-" + phase + ".json"), report);
|
||||
write(Path.of("diagnostics/aerial24/" + phase + ".json"), report);
|
||||
release.run(); helper.succeed();
|
||||
} catch (Exception error) {
|
||||
release.run(); failure(report, error); throw new IllegalStateException("Aerial24 failed: " + error.getMessage(), error);
|
||||
}
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
}
|
||||
|
||||
private static void verifyClaims(GameTestHelper helper, SanctuaryChunkGenerator generator, AerialPlan24 plan, Map<String, Object> report) {
|
||||
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var views = new LinkedHashMap<String, Object>();
|
||||
for (var site : plan.sites()) {
|
||||
if (site.merchant()) {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
|
||||
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
|
||||
} else {
|
||||
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
|
||||
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
|
||||
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
|
||||
int highest = 0;
|
||||
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) { highest = y; break; }
|
||||
helper.assertTrue(site.minY() - highest - 1 >= AerialPlanner24.MIN_ISLET_GAP, "At least sixteen air blocks separate ground and suspended rock");
|
||||
}
|
||||
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
|
||||
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
|
||||
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance, "hull_to_shore", AerialPlanner24.shoreDistance(site),
|
||||
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
|
||||
}
|
||||
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
|
||||
"Unclaimed space remains available; there is no blanket exclusion ring");
|
||||
report.put("look_views", views);
|
||||
}
|
||||
|
||||
private static List<Villager> occupants(ServerLevel level, AerialPlan24 plan) {
|
||||
var expected = plan.sites().stream().filter(AerialPlan24.Site::merchant).map(AerialGeometry24::occupantUuid).collect(java.util.stream.Collectors.toSet());
|
||||
return level.getEntitiesOfClass(Villager.class, new AABB(-2048, 0, -2048, 2048, 384, 2048))
|
||||
.stream().filter(villager -> expected.contains(villager.getUUID())).toList();
|
||||
}
|
||||
|
||||
private static Map<String, StructureStart> starts(GameTestHelper helper, ServerLevel level, AerialPlan24 plan) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var result = new TreeMap<String, StructureStart>();
|
||||
for (var site : plan.sites()) {
|
||||
var type = registry.get(Identifier.parse(AerialStructures24.structureId(site))).orElseThrow().value();
|
||||
var start = level.getChunk(site.startChunkX(), site.startChunkZ(), ChunkStatus.FULL, true).getStartForStructure(type);
|
||||
helper.assertTrue(start != null && start.isValid(), "Planned aerial site has a real persisted native start: " + site.id());
|
||||
helper.assertTrue(start.getPieces().stream().filter(piece -> piece instanceof AerialPiece24 aerial && aerial.site().equals(site)).count() == 1,
|
||||
"Exactly one piece retains the site's immutable descriptor");
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var tag = start.createTag(context, start.getChunkPos());
|
||||
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, start.getChunkPos())), "Aerial start round-trips through native serialization");
|
||||
if (site.merchant()) helper.assertTrue(start.getPieces().stream().filter(AerialPiece24.class::isInstance).map(AerialPiece24.class::cast)
|
||||
.allMatch(AerialPiece24::occupantPlaced), "Merchant placement flag is stored after its first creation");
|
||||
result.put(site.id(), start);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Map<BlockPos, BlockState>> inspect(GameTestHelper helper, ServerLevel level, AerialPlan24 plan,
|
||||
boolean fresh, Map<String, Object> report) {
|
||||
var expected = new LinkedHashMap<String, Map<BlockPos, BlockState>>(); int chestCount = 0, waterSources = 0;
|
||||
var vegetation = new TreeMap<String, Object>();
|
||||
var shores = new TreeMap<String, Object>(); report.put("shore_witnesses", shores);
|
||||
for (var site : plan.sites()) {
|
||||
var model = render(site); expected.put(site.id(), model);
|
||||
var shore = new BlockPos(site.shoreX(), site.shoreY(), site.shoreZ());
|
||||
var column = new TreeMap<Integer, String>();
|
||||
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
|
||||
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
|
||||
if (site.merchant()) {
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
} else {
|
||||
// Natural trees may occupy the mathematical ground reference below an islet.
|
||||
// Observe a real nearby walking position without removing that vegetation.
|
||||
var viewpoint = nearbyGroundView(level, shore);
|
||||
helper.assertTrue(viewpoint != null, "A walkable ground viewpoint exists within twelve horizontal blocks below the suspended islet: " + site.id());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column,
|
||||
"walkable_viewpoint", coordinates(viewpoint), "reference_obstruction", level.getBlockState(shore.above()).toString()));
|
||||
}
|
||||
var chestAt = AerialGeometry24.chest(site);
|
||||
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
|
||||
if (fresh) {
|
||||
helper.assertTrue(AerialRenderer24.loot(site).equals(chest.getLootTable()), "Aerial chest retains its unopened site-specific loot table");
|
||||
for (var entry : model.entrySet()) if (!entry.getValue().isAir() && !entry.getValue().is(Blocks.WATER))
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Native FULL keeps authored material at " + site.id() + " " + entry.getKey());
|
||||
for (var pos : AerialGeometry24.interiorChecks(site)) {
|
||||
helper.assertTrue(level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()
|
||||
&& level.getBlockState(pos.above()).getCollisionShape(level, pos.above()).isEmpty(), "Usable two-block headroom at " + site.id());
|
||||
helper.assertTrue(!level.getBlockState(pos.below()).getCollisionShape(level, pos.below()).isEmpty(), "Interior stands on a real floor");
|
||||
}
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) for (int dy = 1; dy <= 3; dy++)
|
||||
helper.assertTrue(level.getBlockState(chestAt.above(dy)).isSolid(), "Mineral chest needs excavation through three layers");
|
||||
} else helper.assertTrue(chest.getLootTable() == null && chest.isEmpty(), "Looted chest never refills on reload");
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET)
|
||||
vegetation.put(site.id(), vegetation(helper, level, site));
|
||||
if (site.waterfall()) {
|
||||
var source = AerialGeometry24.waterfallSource(site);
|
||||
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
|
||||
}
|
||||
// Root terrain and its own trees may now exist outside these aerial envelopes.
|
||||
// Chunk clipping is checked against every authored position; do not confuse legal
|
||||
// surrounding terrain with a structure leak by requiring a whole exterior air column.
|
||||
helper.assertTrue(model.keySet().stream().allMatch(site.bounds()::isInside), "Every proposed structure block is inside its finite envelope");
|
||||
var exterior = new ArrayList<Map<String, Object>>();
|
||||
for (var at : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2),
|
||||
new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
|
||||
report.put(site.id() + "_exterior_observations", exterior);
|
||||
}
|
||||
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
|
||||
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
|
||||
report.put("vegetation", vegetation);
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static BlockPos nearbyGroundView(ServerLevel level, BlockPos reference) {
|
||||
for (int radius = 0; radius <= 12; radius++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != radius || Math.hypot(dx, dz) > 12) continue;
|
||||
for (int dy = 8; dy >= -8; dy--) {
|
||||
var at = reference.offset(dx, dy, dz);
|
||||
var floor = level.getBlockState(at.below());
|
||||
if (!floor.isSolid() || floor.is(net.minecraft.tags.BlockTags.LEAVES) || floor.is(net.minecraft.tags.BlockTags.LOGS)) continue;
|
||||
if (level.getBlockState(at).getCollisionShape(level, at).isEmpty()
|
||||
&& level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()
|
||||
&& level.getFluidState(at).isEmpty() && level.getFluidState(at.above()).isEmpty()) return at;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
|
||||
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
"Tree headroom preserves the original stone height");
|
||||
int surface = 0, grass = 0;
|
||||
for (int x = 0; x < site.width(); x++) for (int z = 0; z < site.depth(); z++) {
|
||||
int top = AerialGeometry24.terrainTop(site, x, z);
|
||||
if (!AerialGeometry24.mineralBody(site, x, top, z)) continue;
|
||||
surface++;
|
||||
if (level.getBlockState(AerialGeometry24.world(site, x, top, z)).is(Blocks.GRASS_BLOCK)) grass++;
|
||||
}
|
||||
helper.assertTrue(grass * 2 > surface, "Most real intact surface columns carry grass: " + site.id());
|
||||
var trees = AerialGeometry24.trees(site);
|
||||
helper.assertTrue(!trees.isEmpty(), "At least one real tree on each mineral islet");
|
||||
for (var tree : trees) {
|
||||
var ground = AerialGeometry24.world(site, tree.x(), tree.groundY(), tree.z());
|
||||
var floor = level.getBlockState(ground);
|
||||
helper.assertTrue(floor.is(Blocks.GRASS_BLOCK) || floor.is(Blocks.DIRT), "Tree stands on soil, never in the air");
|
||||
for (int y = 1; y <= tree.trunkHeight(); y++)
|
||||
helper.assertTrue(level.getBlockState(ground.above(y)).is(tree.birch() ? Blocks.BIRCH_LOG : Blocks.OAK_LOG), "Native tree trunk stays continuous");
|
||||
var crown = ground.above(tree.trunkHeight() + 1);
|
||||
var leaves = level.getBlockState(crown);
|
||||
helper.assertTrue(site.bounds().isInside(crown) && leaves.is(tree.birch() ? Blocks.BIRCH_LEAVES : Blocks.OAK_LEAVES)
|
||||
&& !leaves.getValue(BlockStateProperties.PERSISTENT) && leaves.getValue(BlockStateProperties.DISTANCE) < 7,
|
||||
"Complete natural crown survives native chunk decoration and reload");
|
||||
}
|
||||
return Map.of("rock_height", site.rockHeight(), "headroom", AerialPlan24.VEGETATION_HEADROOM,
|
||||
"surface_columns", surface, "grass_columns", grass, "grass_percent", 100.0 * grass / surface, "trees", trees);
|
||||
}
|
||||
|
||||
private static Map<String, Object> mutate(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, List<Villager> occupants,
|
||||
Map<String, Map<BlockPos, BlockState>> models) {
|
||||
var emptied = new ArrayList<List<Integer>>(); var removed = new ArrayList<List<Integer>>(); var loot = new TreeMap<String, Integer>();
|
||||
for (var site : plan.sites()) {
|
||||
var at = AerialGeometry24.chest(site); var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
|
||||
chest.unpackLootTable(null); int quantity = 0;
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) quantity += chest.getItem(slot).getCount();
|
||||
helper.assertTrue(quantity > 0, "Site-specific chest produces usable vanilla loot"); loot.put(site.id(), quantity);
|
||||
chest.clearContent(); chest.setChanged(); emptied.add(coordinates(at));
|
||||
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
|
||||
var ores = models.get(site.id()).entrySet().stream().filter(entry -> ore(entry.getValue())).sorted(Map.Entry.comparingByKey(
|
||||
Comparator.<BlockPos>comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ))).limit(16).toList();
|
||||
helper.assertTrue(!ores.isEmpty(), "Each mineral island contains actual finite veins");
|
||||
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
|
||||
}
|
||||
}
|
||||
var ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow();
|
||||
var demolition = AerialGeometry24.entry(ship).below();
|
||||
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
|
||||
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
|
||||
var extracted = new TreeMap<String, List<Double>>(); int i = 0;
|
||||
for (var villager : occupants.stream().sorted(Comparator.comparing(v -> v.getUUID().toString())).toList()) {
|
||||
// This fixture parks extracted merchants in an existing root chunk, outside both ships.
|
||||
villager.setNoAi(true); villager.setNoGravity(true); villager.setDeltaMovement(Vec3.ZERO);
|
||||
villager.snapTo(4.5 + i++ * 4, 330, 4.5); villager.setPersistenceRequired();
|
||||
extracted.put(villager.getUUID().toString(), List.of(villager.getX(), villager.getY(), villager.getZ()));
|
||||
}
|
||||
return Map.of("emptied_chests", emptied, "removed_blocks", removed, "extracted_merchants", extracted, "loot_items_obtained", loot);
|
||||
}
|
||||
|
||||
private static void verifyMutations(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, JsonObject mutations, List<Villager> occupants) {
|
||||
for (var value : mutations.getAsJsonArray("removed_blocks")) {
|
||||
var pos = position(value.getAsJsonArray()); helper.assertTrue(level.getBlockState(pos).isAir(), "Mined ore and dismantled floor remain absent after reload");
|
||||
}
|
||||
var extracted = mutations.getAsJsonObject("extracted_merchants");
|
||||
for (var villager : occupants) {
|
||||
var at = extracted.getAsJsonArray(villager.getUUID().toString());
|
||||
helper.assertTrue(at != null && Math.abs(villager.getX() - at.get(0).getAsDouble()) < .01
|
||||
&& Math.abs(villager.getY() - at.get(1).getAsDouble()) < .01 && Math.abs(villager.getZ() - at.get(2).getAsDouble()) < .01,
|
||||
"Extracted merchant stays outside its original ship without respawning");
|
||||
}
|
||||
for (var site : plan.sites()) if (site.merchant()) helper.assertTrue(level.getEntitiesOfClass(Villager.class, AABB.of(site.bounds())).isEmpty(),
|
||||
"The visited ship does not replenish an extracted merchant");
|
||||
}
|
||||
|
||||
private static void verifyLocator(GameTestHelper helper, ServerLevel level, AerialPlan24 plan, Map<String, StructureStart> starts) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var holders = new ArrayList<Holder<Structure>>();
|
||||
plan.sites().stream().map(AerialStructures24::structureId).distinct().forEach(id -> holders.add(registry.get(Identifier.parse(id)).orElseThrow()));
|
||||
holders.add(registry.get(Identifier.withDefaultNamespace("stronghold")).orElseThrow());
|
||||
var wanted = HolderSet.direct(holders); var origin = plan.sites().getFirst().locator();
|
||||
var activeBefore = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands();
|
||||
int before = starts.values().stream().mapToInt(StructureStart::getReferences).sum();
|
||||
var found = ExpansionStructures23.findNearest(level, wanted, origin, 100, false);
|
||||
helper.assertTrue(found != null, "Mixed vanilla/aerial locator finds a real accepted start");
|
||||
helper.assertTrue(starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before, "Ordinary locate never marks a site referenced");
|
||||
var referenced = ExpansionStructures23.findNearest(level, wanted, origin, 100, true);
|
||||
helper.assertTrue(referenced != null && starts.values().stream().mapToInt(StructureStart::getReferences).sum() == before + 1,
|
||||
"Mixed map lookup references exactly its one winning aerial start");
|
||||
helper.assertTrue(activeBefore.equals(((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().islands()), "Locator never activates an expansion");
|
||||
}
|
||||
|
||||
private static Map<BlockPos, BlockState> render(AerialPlan24.Site site) {
|
||||
var result = new HashMap<BlockPos, BlockState>();
|
||||
AerialRenderer24.render(site, new LostCityRenderer.BlockWriter() {
|
||||
public void set(int x, int y, int z, BlockState state) { result.put(new BlockPos(site.minX() + x, site.minY() + y, site.minZ() + z), state); }
|
||||
public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { set(x, y, z, Blocks.CHEST.defaultBlockState()); }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
private static Map<String, Map<String, Integer>> countSites(ServerLevel level, AerialPlan24 plan) {
|
||||
var all = new TreeMap<String, Map<String, Integer>>();
|
||||
for (var site : plan.sites()) {
|
||||
var counts = new TreeMap<String, Integer>();
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) for (int y = site.minY(); y <= site.maxY(); y++) {
|
||||
var state = level.getBlockState(new BlockPos(x, y, z));
|
||||
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
}
|
||||
all.put(site.id(), counts);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
private static boolean ore(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"); }
|
||||
private static Map<String, String> tags(ServerLevel level, Map<String, StructureStart> starts) {
|
||||
var result = new TreeMap<String, String>(); var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
starts.forEach((id, start) -> result.put(id, start.createTag(context, start.getChunkPos()).toString())); return result;
|
||||
}
|
||||
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos position(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Map<String, byte[]> files(Path directory) throws java.io.IOException {
|
||||
var found = new TreeMap<String, byte[]>();
|
||||
if (Files.exists(directory)) try (var paths = Files.walk(directory)) {
|
||||
for (var path : paths.filter(Files::isRegularFile).toList()) found.put(directory.relativize(path).toString(), Files.readAllBytes(path));
|
||||
}
|
||||
return found;
|
||||
}
|
||||
private static boolean equalFiles(Map<String, byte[]> a, Map<String, byte[]> b) {
|
||||
return a.keySet().equals(b.keySet()) && a.entrySet().stream().allMatch(entry -> Arrays.equals(entry.getValue(), b.get(entry.getKey())));
|
||||
}
|
||||
private static void write(Path path, Map<String, Object> report) throws java.io.IOException { Files.createDirectories(path.toAbsolutePath().getParent()); Files.writeString(path, JSON.toJson(report)); }
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("failure", error.toString());
|
||||
var stack = new java.io.StringWriter(); error.printStackTrace(new java.io.PrintWriter(stack)); report.put("stacktrace", stack.toString());
|
||||
SanctuaryMod.LOGGER.error("Aerial24 fixture failed at {}", report.get("stage"), error);
|
||||
try { write(Path.of("diagnostics/aerial24/failure.json"), report); } catch (java.io.IOException failed) { error.addSuppressed(failed); }
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// Frozen test oracle from a26b72f PopulationIslandDensity.java.
|
||||
// Original SHA-256: fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1
|
||||
// Only package/imports/class name differ; do not optimize this reference.
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record Alpha10PopulationDensityReference(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
public static final MapCodec<Alpha10PopulationDensityReference> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(Alpha10PopulationDensityReference::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(Alpha10PopulationDensityReference::distortion),
|
||||
DensityFunction.CODEC.fieldOf("sculpt").forGetter(Alpha10PopulationDensityReference::sculpt),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(Alpha10PopulationDensityReference::detail),
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(Alpha10PopulationDensityReference::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(Alpha10PopulationDensityReference::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(Alpha10PopulationDensityReference::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(Alpha10PopulationDensityReference::capacity)
|
||||
).apply(instance, Alpha10PopulationDensityReference::new));
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = terrain.compileSampler(context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return new DensitySampler() {
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
|
||||
int localY = y - 64;
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new Alpha10PopulationDensityReference(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<Alpha10PopulationDensityReference> codec() { return CODEC; }
|
||||
}
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.expansion.SwampHutPlacement28;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
/** Observes normal root generation only. No start injection, fake room, expansion or seed search. */
|
||||
public final class Alpha28NativeStructuresGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path OUT = Path.of("diagnostics/native28");
|
||||
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void actualNativeStructures(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try {
|
||||
var level = helper.getLevel(); var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", host.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
if (!level.structureManager().shouldGenerateStructures()) {
|
||||
report.put("scope", "Native structures globally disabled"); report.put("passed", true); write(report); helper.succeed(); return;
|
||||
}
|
||||
var root = host.source().islands().stream().filter(island -> island.origin()).findFirst().orElseThrow();
|
||||
var candidates = ExpansionStructures23.candidates(root).stream().limit(24).toList();
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
|
||||
var firstTwelve = new TreeMap<String, Integer>(); int candidateIndex = 0;
|
||||
for (var candidate : candidates) {
|
||||
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
for (var start : chunk.getAllStarts().values()) {
|
||||
if (!start.isValid()) continue;
|
||||
String id = registry.getKey(start.getStructure()).toString(); observed.merge(id, 1, Integer::sum);
|
||||
if (candidateIndex < 12) firstTwelve.merge(id, 1, Integer::sum);
|
||||
if (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
|
||||
}
|
||||
candidateIndex++;
|
||||
}
|
||||
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
|
||||
report.put("first_twelve_starts", firstTwelve);
|
||||
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29") && found.keySet().stream().noneMatch(id -> id.contains("mineshaft"))) {
|
||||
var islandsBefore = List.copyOf(host.source().islands());
|
||||
var wanted = net.minecraft.core.HolderSet.direct(registry.get(Identifier.parse("minecraft:mineshaft")).orElseThrow(),
|
||||
registry.get(Identifier.parse("minecraft:mineshaft_mesa")).orElseThrow());
|
||||
// Same finite real-start path as /locate; no injected plan, reference increment or expansion.
|
||||
var located = ExpansionStructures23.findNearest(level, wanted, BlockPos.ZERO, 8, false);
|
||||
helper.assertTrue(islandsBefore.equals(host.source().islands()), "Native /locate never adds an expansion or changes the island descriptors");
|
||||
var locateProof = new LinkedHashMap<String, Object>();
|
||||
locateProof.put("invoked", true); locateProof.put("found", located != null);
|
||||
locateProof.put("islands_unchanged", true); locateProof.put("skip_referenced", false);
|
||||
locateProof.put("root_candidates_available", ExpansionStructures23.candidates(root).size());
|
||||
report.put("locate_mineshaft", locateProof);
|
||||
if (located != null) {
|
||||
String id = located.getSecond().unwrapKey().orElseThrow().identifier().toString();
|
||||
for (var candidate : ExpansionStructures23.candidates(root)) {
|
||||
var start = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true)
|
||||
.getStartForStructure(located.getSecond().value());
|
||||
if (start != null && start.isValid() && start.getPieces().getFirst().getLocatorPosition().equals(located.getFirst())) {
|
||||
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.chunk().x(), candidate.chunk().z())); break;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(found.containsKey(id), "The native /locate result belongs to a real saved start");
|
||||
locateProof.put("id", id); locateProof.put("position", located.getFirst().toShortString());
|
||||
}
|
||||
} else report.put("locate_mineshaft", Map.of("invoked", false));
|
||||
var full = new LinkedHashSet<ChunkPos>();
|
||||
// Visit actual corridor bodies (including both endpoint chunks), not just four
|
||||
// piece centres that could all belong to unadorned rooms. No graph is truncated.
|
||||
var pieceContext = StructurePieceSerializationContext.fromLevel(level);
|
||||
int mineFamilies = (int) found.keySet().stream().filter(id -> id.contains("mineshaft")).count();
|
||||
for (var entry : found.entrySet()) {
|
||||
full.add(entry.getValue().getChunkPos());
|
||||
var pieces = entry.getValue().getPieces();
|
||||
if (entry.getKey().contains("mineshaft")) pieces = pieces.stream()
|
||||
.filter(piece -> piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor)
|
||||
.sorted(Comparator.comparingInt(piece -> {
|
||||
var tag = piece.createTag(pieceContext);
|
||||
return tag.getBoolean("hr").orElse(false) ? 0 : tag.getBoolean("sc").orElse(false) ? 1 : 2;
|
||||
})).limit(mineFamilies > 1 ? 2 : 4).toList();
|
||||
for (var piece : pieces) {
|
||||
var proposed = new LinkedHashSet<>(full); piece.getBoundingBox().intersectingChunks().forEach(proposed::add);
|
||||
if (proposed.size() <= 24) full.addAll(proposed);
|
||||
}
|
||||
}
|
||||
// A few additional real generation chunks can contain native monster_room attempts.
|
||||
for (var candidate : candidates) {
|
||||
if (full.size() >= 16) break;
|
||||
full.add(candidate.chunk());
|
||||
}
|
||||
helper.assertTrue(full.size() <= 24, "Bounded native structure witness requests at most24 FULL chunks");
|
||||
var iterator = full.iterator(); Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (iterator.hasNext()) {
|
||||
var at = iterator.next(); report.put("stage", "FULL " + at);
|
||||
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
inspect(helper, level, found, full, report);
|
||||
report.put("passed", true); write(report); helper.succeed();
|
||||
} catch (Exception error) { failure(report, error); throw new IllegalStateException("Native28 witness failed", error); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
}
|
||||
|
||||
private static void inspect(GameTestHelper helper, ServerLevel level, Map<String, StructureStart> found,
|
||||
Set<ChunkPos> full, Map<String, Object> report) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var proofs = new LinkedHashMap<String, Object>();
|
||||
var census = StructureCensusService.snapshot(level.getServer()).evidence().sites().values();
|
||||
var monsterPositions = new HashSet<BlockPos>();
|
||||
for (var site : census) {
|
||||
var key = site.key();
|
||||
if (key.id().equals(StructureCensusMemory.MONSTER_ROOM) && key.dimension().equals(level.dimension().identifier().toString()))
|
||||
monsterPositions.add(new BlockPos(key.x(), key.y(), key.z()));
|
||||
}
|
||||
for (var entry : found.entrySet()) {
|
||||
var expected = entry.getValue(); var cp = expected.getChunkPos();
|
||||
var actual = level.getChunk(cp.x(), cp.z(), ChunkStatus.FULL, true)
|
||||
.getStartForStructure(registry.getValue(Identifier.parse(entry.getKey())));
|
||||
helper.assertTrue(actual != null && actual.isValid(), "The real selected native start survives FULL: " + entry.getKey());
|
||||
var tag = actual.createTag(context, cp);
|
||||
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Native piece NBT roundtrip: " + entry.getKey());
|
||||
var counts = new TreeMap<String, Integer>(); var positions = new HashSet<BlockPos>();
|
||||
var spawners = new ArrayList<Object>(); var corridorFlags = new ArrayList<Object>();
|
||||
var spawnerTypes = new HashMap<BlockPos, String>();
|
||||
for (var piece : actual.getPieces()) {
|
||||
var b = piece.getBoundingBox();
|
||||
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
|
||||
var flags = piece.createTag(context);
|
||||
corridorFlags.add(Map.of("box", b.toString(), "fully_visited", b.intersectingChunks().allMatch(full::contains),
|
||||
"rails", flags.getBoolean("hr").orElse(false), "spider_corridor", flags.getBoolean("sc").orElse(false),
|
||||
"spawner_placed", flags.getBoolean("hps").orElse(false)));
|
||||
}
|
||||
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
|
||||
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
|
||||
for (int y = b.minY() - 1; y <= b.maxY() + 1; y++) {
|
||||
BlockPos pos = new BlockPos(x, y, z); if (!positions.add(pos)) continue;
|
||||
var state = level.getBlockState(pos);
|
||||
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
if (state.is(Blocks.SPAWNER)) {
|
||||
helper.assertTrue(level.getBlockEntity(pos) instanceof SpawnerBlockEntity, "A mine spawner has its actual native block entity");
|
||||
var saved = ((SpawnerBlockEntity) level.getBlockEntity(pos)).saveWithFullMetadata(level.registryAccess());
|
||||
String mob = saved.getCompoundOrEmpty("SpawnData").getCompoundOrEmpty("entity").getStringOr("id", "");
|
||||
spawnerTypes.put(pos, mob);
|
||||
String origin = monsterPositions.contains(pos) ? "monster_room" : "mineshaft";
|
||||
if (entry.getKey().contains("mineshaft")) {
|
||||
if (origin.equals("monster_room")) helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").contains(mob),
|
||||
"A recorded vanilla dungeon inside the mine retains its own monster type");
|
||||
else {
|
||||
var flags = piece.createTag(context);
|
||||
helper.assertTrue(mob.equals("minecraft:cave_spider") && flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false),
|
||||
"A native mine spawner agrees with its cave-spider piece flags");
|
||||
}
|
||||
}
|
||||
spawners.add(Map.of("position", pos.toShortString(), "origin", origin, "entity", mob, "nbt", saved.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
|
||||
var flags = piece.createTag(context);
|
||||
if (flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false) && b.intersectingChunks().allMatch(full::contains)) {
|
||||
boolean actualSpider = spawnerTypes.entrySet().stream().anyMatch(e -> b.isInside(e.getKey()) && e.getValue().equals("minecraft:cave_spider"));
|
||||
// A later successful native monster_room may replace a mine's spawner. Its real
|
||||
// census coordinate must then lie on this corridor's native spawner centreline.
|
||||
boolean recordedReplacement = false;
|
||||
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? b.getZSpan() : b.getXSpan();
|
||||
for (int z = 1; z < length; z++) {
|
||||
var at = SwampHutPlacement28.worldPosition(piece,1,0,z);
|
||||
if (monsterPositions.contains(at) && spawnerTypes.containsKey(at)) recordedReplacement = true;
|
||||
}
|
||||
helper.assertTrue(actualSpider || recordedReplacement, "A fully visited sc/hps corridor has its actual cave-spider spawner or an evidenced later vanilla dungeon");
|
||||
}
|
||||
}
|
||||
if (entry.getKey().equals("minecraft:swamp_hut")) for (var pier : SwampHutPlacement28.piers(piece)) {
|
||||
var state = level.getBlockState(pier);
|
||||
helper.assertTrue(state.is(Blocks.OAK_LOG) || state.isSolid(), "A native hut pier has wood or a real solid foot: " + pier);
|
||||
}
|
||||
}
|
||||
if (entry.getKey().contains("mineshaft")) helper.assertTrue(counts.entrySet().stream()
|
||||
.filter(e -> e.getKey().endsWith("_planks") || e.getKey().endsWith("_fence") || e.getKey().equals("minecraft:rail"))
|
||||
.mapToInt(Map.Entry::getValue).sum() > 0, "A selected mine creates actual native timber/rail geometry");
|
||||
proofs.put(entry.getKey(), Map.of("start_chunk", List.of(cp.x(), cp.z()), "pieces", actual.getPieces().size(),
|
||||
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString(),
|
||||
"corridor_flags", corridorFlags, "spawners", spawners));
|
||||
}
|
||||
var rooms = new ArrayList<Object>();
|
||||
for (var site : census) {
|
||||
var key = site.key();
|
||||
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
|
||||
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
|
||||
if (!full.contains(cp)) continue;
|
||||
var at = new BlockPos(key.x(), key.y(), key.z());
|
||||
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
|
||||
var spawnerNbt = ((SpawnerBlockEntity) level.getBlockEntity(at)).saveWithFullMetadata(level.registryAccess());
|
||||
helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").stream().anyMatch(spawnerNbt.toString()::contains), "Vanilla dungeon retains a real vanilla monster type");
|
||||
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true, "spawner_nbt", spawnerNbt.toString()));
|
||||
}
|
||||
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
|
||||
boolean mineFound = found.keySet().stream().anyMatch(id -> id.contains("mineshaft"));
|
||||
report.put("mineshaft_found", mineFound);
|
||||
report.put("mineshaft_required", Boolean.getBoolean("sanctuary.test.requireMineshaft29"));
|
||||
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
|
||||
report.put("monster_room_found", !rooms.isEmpty());
|
||||
report.put("scope", "Sample of24 normal start candidates, optional real /locate when a mine is required, and at most24 requested FULL chunks; the first12 remain separately reported. Corridor flags and optional spawners do not impose random-content quotas");
|
||||
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29"))
|
||||
helper.assertTrue(mineFound, "The required mine witness has a real native start and inspected FULL timber/rail geometry");
|
||||
}
|
||||
private static void write(Map<String, Object> report) throws Exception {
|
||||
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
|
||||
}
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("error", error.toString());
|
||||
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
|
||||
catch (Exception io) { error.addSuppressed(io); }
|
||||
SanctuaryMod.LOGGER.error("Native28 witness failed", error);
|
||||
}
|
||||
}
|
||||
-384
@@ -1,384 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledge;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeEvents;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeFacet;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeService;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeStore;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.CommonListenerCookie;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.inventory.ChestMenu;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Exercise native actions so invalid placements and death drops cannot unlock false history. */
|
||||
public final class BlockKnowledgeGameTests {
|
||||
@GameTest(maxTicks = 100)
|
||||
public void placementCountsConfirmedStatesAndRejectsFailure(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
BlockPos support = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
helper.getLevel().setBlockAndUpdate(support, Blocks.STONE.defaultBlockState());
|
||||
helper.getLevel().setBlockAndUpdate(support.above(), Blocks.AIR.defaultBlockState());
|
||||
helper.getLevel().setBlockAndUpdate(support.east(), Blocks.AIR.defaultBlockState());
|
||||
|
||||
helper.assertTrue(place(player, Items.COBBLESTONE, support, Direction.UP),
|
||||
"A valid block placement must succeed");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.COBBLESTONE).placed() == 1,
|
||||
"A successful placement must be recorded exactly once");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.COBBLESTONE).possessed(),
|
||||
"The final held block must be observed before placement consumes it");
|
||||
|
||||
helper.assertTrue(place(player, Items.TORCH, support, Direction.EAST),
|
||||
"A torch must attach to the side of a solid support");
|
||||
helper.assertTrue(helper.getLevel().getBlockState(support.east()).is(Blocks.WALL_TORCH),
|
||||
"The placement fixture must produce a wall torch");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.WALL_TORCH).placed() == 1,
|
||||
"Placement history must use the actual wall block");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.TORCH).placed() == 0,
|
||||
"The item form must not receive a second placement");
|
||||
|
||||
BlockPos invalidSupport = helper.absolutePos(new BlockPos(3, 1, 1));
|
||||
helper.getLevel().setBlockAndUpdate(invalidSupport, Blocks.STONE.defaultBlockState());
|
||||
helper.getLevel().setBlockAndUpdate(invalidSupport.above(), Blocks.AIR.defaultBlockState());
|
||||
helper.assertTrue(!place(player, Items.OAK_SAPLING, invalidSupport, Direction.UP),
|
||||
"A sapling cannot be placed on stone");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.OAK_SAPLING).placed() == 0,
|
||||
"A failed item use must not count as a placement");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void vanillaMiningAndVoluntaryDropsRemainAuthoritative(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
BlockPos mined = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
helper.getLevel().setBlockAndUpdate(mined, Blocks.STONE.defaultBlockState());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
helper.assertTrue(player.gameMode.destroyBlock(mined), "The test player must mine stone");
|
||||
helper.assertTrue(player.getStats().getValue(Stats.BLOCK_MINED.get(Blocks.STONE)) == 1,
|
||||
"The successful harvest must increment Minecraft's mined counter");
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.STONE).mined() == 1,
|
||||
"Sanctuary must expose Minecraft's counter without double counting");
|
||||
|
||||
helper.getLevel().setBlockAndUpdate(mined, Blocks.STONE.defaultBlockState());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
player.gameMode.destroyBlock(mined);
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.STONE).mined() == 1,
|
||||
"Breaking stone with the wrong tool must preserve vanilla mining semantics");
|
||||
|
||||
player.drop(new ItemStack(Items.DIRT, 3), true, Prediction.PREDICTED);
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.DIRT).dropped() == 3,
|
||||
"Voluntary drops must count individual items exactly once");
|
||||
player.getInventory().setItem(0, new ItemStack(Items.DIRT, 5));
|
||||
player.getInventory().dropAll();
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.DIRT).dropped() == 3,
|
||||
"The inventory's death-drop path must not count as voluntary drops");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void cursorPossessionSurvivesImmediateReturnToChest(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
SimpleContainer chest = new SimpleContainer(27);
|
||||
chest.setItem(0, new ItemStack(Items.EMERALD_BLOCK, 5));
|
||||
ChestMenu menu = ChestMenu.threeRows(1, player.getInventory(), chest);
|
||||
player.containerMenu = menu;
|
||||
helper.assertTrue(!BlockKnowledgeService.knowledge(player, Blocks.EMERALD_BLOCK).possessed(),
|
||||
"An unopened chest stack must not count as player possession");
|
||||
|
||||
// No service query or tick between taking and returning the stack.
|
||||
menu.clicked(0, 0, ContainerInput.PICKUP, player);
|
||||
menu.clicked(0, 0, ContainerInput.PICKUP, player);
|
||||
var knowledge = BlockKnowledgeService.knowledge(player, Blocks.EMERALD_BLOCK);
|
||||
helper.assertTrue(knowledge.possessed(),
|
||||
"A stack briefly held on the cursor must remain in possession history");
|
||||
helper.assertTrue(knowledge.carried() == 0,
|
||||
"Returning the stack must remove current stock without erasing its history");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void gazeStopsAtFirstBlockAndSpectatorsRevealNothing(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
Vec3 feet = helper.absoluteVec(new Vec3(1.5, 2, 1.5));
|
||||
player.setPos(feet);
|
||||
player.setYRot(0);
|
||||
player.setXRot(0);
|
||||
BlockPos eye = BlockPos.containing(player.getEyePosition());
|
||||
// The observer deliberately never requests missing chunks. Load its short ray here,
|
||||
// in the isolated test world, before asking it to observe anything.
|
||||
int minX = (int) Math.floor(player.getX() - 1) >> 4;
|
||||
int maxX = (int) Math.floor(player.getX() + 1) >> 4;
|
||||
int minZ = (int) Math.floor(player.getZ() - 1) >> 4;
|
||||
int maxZ = (int) Math.floor(player.getZ() + BlockKnowledgeService.OBSERVATION_DISTANCE + 1) >> 4;
|
||||
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
|
||||
helper.getLevel().getChunk(x, z);
|
||||
}
|
||||
for (int z = 0; z <= 4; z++) {
|
||||
helper.getLevel().setBlockAndUpdate(eye.offset(0, 0, z), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
BlockPos visible = eye.offset(0, 0, 2);
|
||||
BlockPos hidden = eye.offset(0, 0, 3);
|
||||
helper.getLevel().setBlockAndUpdate(visible, Blocks.STONE.defaultBlockState());
|
||||
helper.getLevel().setBlockAndUpdate(hidden, Blocks.DIAMOND_ORE.defaultBlockState());
|
||||
|
||||
BlockKnowledgeService.observeLook(player);
|
||||
helper.assertTrue(BlockKnowledgeService.knowledge(player, Blocks.STONE).seen(),
|
||||
"The first block on the player's gaze ray must become seen");
|
||||
helper.assertTrue(!BlockKnowledgeService.knowledge(player, Blocks.DIAMOND_ORE).known(),
|
||||
"An ore behind the first block must remain unknown");
|
||||
|
||||
ServerPlayer spectator = (ServerPlayer) helper.makeMockServerPlayer(GameType.SPECTATOR);
|
||||
spectator.setPos(feet);
|
||||
spectator.setYRot(0);
|
||||
spectator.setXRot(0);
|
||||
helper.assertTrue(spectator.isSpectator(), "The observer fixture must be a spectator");
|
||||
BlockKnowledgeService.observeLook(spectator);
|
||||
helper.assertTrue(!BlockKnowledgeService.knowledge(spectator, Blocks.STONE).known()
|
||||
&& !BlockKnowledgeService.knowledge(spectator, Blocks.DIAMOND_ORE).known(),
|
||||
"Spectator gaze must not discover either visible or occluded blocks");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void existingStatsRemainPersonalWithoutInventingSightOrPlacements(GameTestHelper helper) {
|
||||
ServerPlayer first = player(helper);
|
||||
ServerPlayer second = player(helper);
|
||||
// Restore vanilla values directly, without going through an action hook.
|
||||
first.getStats().setValue(first, Stats.BLOCK_MINED.get(Blocks.DIAMOND_ORE), 7);
|
||||
first.getStats().setValue(first, Stats.ITEM_DROPPED.get(Items.EMERALD_BLOCK), 4);
|
||||
first.getStats().setValue(first, Stats.ITEM_PICKED_UP.get(Items.REDSTONE_BLOCK), 9);
|
||||
var mined = BlockKnowledgeService.knowledge(first, Blocks.DIAMOND_ORE);
|
||||
var dropped = BlockKnowledgeService.knowledge(first, Blocks.EMERALD_BLOCK);
|
||||
var pickedUp = BlockKnowledgeService.knowledge(first, Blocks.REDSTONE_BLOCK);
|
||||
helper.assertTrue(mined.mined() == 7 && !mined.possessed() && !mined.seen() && mined.placed() == 0,
|
||||
"A historical mining counter must not invent possession, sight or placement evidence");
|
||||
helper.assertTrue(dropped.dropped() == 4 && dropped.possessed() && !dropped.seen() && dropped.placed() == 0,
|
||||
"Historical drops imply possession but cannot reconstruct sight or placements");
|
||||
helper.assertTrue(pickedUp.possessed() && pickedUp.dropped() == 0 && !pickedUp.seen() && pickedUp.placed() == 0,
|
||||
"Historical pickups imply possession without inventing other actions");
|
||||
helper.assertTrue(mined.carried() == 0 && dropped.carried() == 0 && pickedUp.carried() == 0,
|
||||
"Historical counters must not create current inventory stock");
|
||||
|
||||
BlockKnowledgeService.recordSeen(first, Blocks.GOLD_BLOCK);
|
||||
helper.assertTrue(!BlockKnowledgeService.knowledge(second, Blocks.DIAMOND_ORE).known()
|
||||
&& !BlockKnowledgeService.knowledge(second, Blocks.EMERALD_BLOCK).known()
|
||||
&& !BlockKnowledgeService.knowledge(second, Blocks.REDSTONE_BLOCK).known()
|
||||
&& !BlockKnowledgeService.knowledge(second, Blocks.GOLD_BLOCK).known(),
|
||||
"Both vanilla evidence and Sanctuary history must stay isolated by player UUID");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void currentStockSumsPersonalSlotsAndLeavesPossessionHistory(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.LAPIS_BLOCK, 4));
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(Items.LAPIS_BLOCK, 2));
|
||||
player.containerMenu.setCarried(new ItemStack(Items.LAPIS_BLOCK, 3));
|
||||
var held = BlockKnowledgeService.knowledge(player, Blocks.LAPIS_BLOCK);
|
||||
helper.assertTrue(held.carried() == 9 && held.possessed(),
|
||||
"Inventory, offhand and cursor must be counted once each");
|
||||
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
|
||||
player.containerMenu.setCarried(ItemStack.EMPTY);
|
||||
var emptied = BlockKnowledgeService.knowledge(player, Blocks.LAPIS_BLOCK);
|
||||
helper.assertTrue(emptied.carried() == 0 && emptied.possessed(),
|
||||
"Empty current stock must retain historical possession");
|
||||
helper.assertTrue(!emptied.seen() && emptied.mined() == 0 && emptied.placed() == 0 && emptied.dropped() == 0,
|
||||
"Inventory evidence must not manufacture other kinds of progression");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void listenersReceiveUpdatedProgressionAndStockChanges(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
var uuid = player.getUUID();
|
||||
List<Notice> notices = new ArrayList<>();
|
||||
BlockKnowledgeEvents.CHANGED.register((source, knowledge, activity) -> {
|
||||
if (source.getUUID().equals(uuid) && knowledge.blockId().equals("minecraft:gold_block")) {
|
||||
notices.add(new Notice(activity, knowledge));
|
||||
}
|
||||
});
|
||||
|
||||
// Vanilla pickup inserts the stack before awarding its statistic. Inventory observation
|
||||
// and the statistic hook must publish one discovery between them, then remain sticky.
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.GOLD_BLOCK));
|
||||
player.awardStat(Stats.ITEM_PICKED_UP.get(Items.GOLD_BLOCK), 1);
|
||||
helper.assertTrue(notices.stream().filter(n -> n.activity() == BlockKnowledgeEvents.Activity.POSSESSED).count() == 1,
|
||||
"One pickup must not duplicate first-possession notifications through its inventory and stat paths");
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.GOLD_BLOCK, 2));
|
||||
player.awardStat(Stats.ITEM_PICKED_UP.get(Items.GOLD_BLOCK), 1);
|
||||
helper.assertTrue(notices.stream().filter(n -> n.activity() == BlockKnowledgeEvents.Activity.POSSESSED).count() == 1,
|
||||
"A later pickup must change stock without publishing another first-possession notification");
|
||||
|
||||
BlockKnowledgeService.recordSeen(player, Blocks.GOLD_BLOCK);
|
||||
BlockKnowledgeService.recordSeen(player, Blocks.GOLD_BLOCK);
|
||||
player.awardStat(Stats.BLOCK_MINED.get(Blocks.GOLD_BLOCK), 2);
|
||||
BlockPos support = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
helper.getLevel().setBlockAndUpdate(support, Blocks.STONE.defaultBlockState());
|
||||
helper.getLevel().setBlockAndUpdate(support.above(), Blocks.AIR.defaultBlockState());
|
||||
helper.assertTrue(place(player, Items.GOLD_BLOCK, support, Direction.UP),
|
||||
"The event fixture must place a gold block");
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.GOLD_BLOCK, 5));
|
||||
BlockKnowledgeService.observeInventory(player);
|
||||
player.getInventory().removeItem(player.getInventory().getSelectedSlot(), 1);
|
||||
player.drop(new ItemStack(Items.GOLD_BLOCK), true, Prediction.PREDICTED);
|
||||
|
||||
helper.assertTrue(notices.stream().filter(n -> n.activity() == BlockKnowledgeEvents.Activity.SEEN).count() == 1,
|
||||
"Repeated sight of the same block must not emit duplicate discoveries");
|
||||
helper.assertTrue(notices.stream().anyMatch(n -> n.activity() == BlockKnowledgeEvents.Activity.MINED && n.knowledge().mined() == 2),
|
||||
"Mining listeners must receive the already updated vanilla counter");
|
||||
helper.assertTrue(notices.stream().anyMatch(n -> n.activity() == BlockKnowledgeEvents.Activity.PLACED && n.knowledge().placed() == 1),
|
||||
"Placement listeners must receive the confirmed placement count");
|
||||
helper.assertTrue(notices.stream().anyMatch(n -> n.activity() == BlockKnowledgeEvents.Activity.POSSESSED && n.knowledge().possessed()),
|
||||
"First possession must be available to future progression listeners");
|
||||
helper.assertTrue(notices.stream().anyMatch(n -> n.activity() == BlockKnowledgeEvents.Activity.DROPPED && n.knowledge().dropped() == 1),
|
||||
"Drop listeners must receive the already updated vanilla counter");
|
||||
helper.assertTrue(notices.stream().anyMatch(n -> n.activity() == BlockKnowledgeEvents.Activity.INVENTORY && n.knowledge().carried() == 4),
|
||||
"Dropping an item must also notify consumers of the changed current stock");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void disconnectFromNetworkThreadSavesAndReloadsOnServer(GameTestHelper helper) throws InterruptedException {
|
||||
ServerPlayer player = player(helper);
|
||||
var server = helper.getLevel().getServer();
|
||||
// A real packet listener, but no player-list registration or live connection. The only
|
||||
// DISCONNECT subscriber in this test runtime is Sanctuary's history lifecycle handler.
|
||||
var listener = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND),
|
||||
player, CommonListenerCookie.createInitial(player.getGameProfile(), false));
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.PRISMARINE, 2));
|
||||
BlockKnowledgeService.recordSeen(player, Blocks.PRISMARINE);
|
||||
BlockKnowledgeService.recordPlaced(player, Blocks.PRISMARINE);
|
||||
var directory = server.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-blocodex");
|
||||
var file = directory.resolve(player.getUUID() + ".json");
|
||||
helper.assertTrue(!Files.exists(file), "Fresh history must still be in memory before its first save");
|
||||
|
||||
AtomicReference<Throwable> workerFailure = new AtomicReference<>();
|
||||
Thread worker = new Thread(() -> {
|
||||
try {
|
||||
if (server.isSameThread()) throw new AssertionError("The disconnect fixture must run outside the server thread");
|
||||
ServerPlayConnectionEvents.DISCONNECT.invoker().onPlayDisconnect(listener, server);
|
||||
} catch (Throwable failure) {
|
||||
workerFailure.set(failure);
|
||||
}
|
||||
}, "sanctuary-blocodex-disconnect-test");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
worker.join(5000);
|
||||
helper.assertTrue(!worker.isAlive(), "The network callback must return without waiting for the server tick");
|
||||
helper.assertTrue(workerFailure.get() == null, "The network callback must safely schedule its work: " + workerFailure.get());
|
||||
helper.assertTrue(!Files.exists(file),
|
||||
"A network-thread disconnect must defer history writes until the server can execute them");
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
|
||||
helper.runAfterDelay(2, () -> {
|
||||
try {
|
||||
helper.assertTrue(Files.isRegularFile(file), "The queued disconnect must save history on the server thread");
|
||||
var disk = new BlockKnowledgeStore(directory, player.getUUID()).load().entry("minecraft:prismarine");
|
||||
helper.assertTrue(disk.seen() && disk.possessed() && disk.placed() == 1,
|
||||
"The saved file must retain all Sanctuary evidence from before disconnect");
|
||||
var reloaded = BlockKnowledgeService.knowledge(player, Blocks.PRISMARINE);
|
||||
helper.assertTrue(reloaded.seen() && reloaded.possessed() && reloaded.placed() == 1 && reloaded.carried() == 0,
|
||||
"A fresh service session must reload history while rebuilding empty current stock");
|
||||
BlockKnowledgeService.recordPlaced(player, Blocks.PRISMARINE);
|
||||
BlockKnowledgeService.flush(server);
|
||||
var updated = new BlockKnowledgeStore(directory, player.getUUID()).load().entry("minecraft:prismarine");
|
||||
helper.assertTrue(updated.placed() == 2,
|
||||
"A reloaded service session must be able to extend and flush its verified history");
|
||||
helper.succeed();
|
||||
} catch (IOException failure) {
|
||||
helper.fail("Cannot read native block knowledge history: " + failure.getMessage());
|
||||
} finally {
|
||||
player.getTextFilter().leave();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void structurePalettesCombinePersonalEvidenceWithAllowedMaterials(GameTestHelper helper) {
|
||||
ServerPlayer player = player(helper);
|
||||
var natural = BlockKnowledgeService.NATURAL_STRUCTURE_MATERIALS;
|
||||
helper.assertTrue(Blocks.STONE.defaultBlockState().is(natural)
|
||||
&& Blocks.OAK_LOG.defaultBlockState().is(natural)
|
||||
&& !Blocks.CONCRETE.pick(net.minecraft.world.item.DyeColor.WHITE).defaultBlockState().is(natural),
|
||||
"The structure palette tag must include stone and logs while excluding concrete");
|
||||
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.STONE, 3));
|
||||
BlockKnowledgeService.observeInventory(player);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
BlockKnowledgeService.recordSeen(player, Blocks.OAK_LOG);
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(Items.CONCRETE.pick(net.minecraft.world.item.DyeColor.WHITE), 5));
|
||||
|
||||
var possessed = BlockKnowledgeService.palette(player, BlockKnowledgeFacet.POSSESSED, natural);
|
||||
helper.assertTrue(possessed.stream().map(BlockKnowledge::blockId).toList().equals(List.of("minecraft:stone")),
|
||||
"A possessed-material palette must retain historical stone, exclude seen-only oak and reject concrete");
|
||||
helper.assertTrue(possessed.getFirst().carried() == 0,
|
||||
"An unlocked material must not invent remaining stock after its items are removed");
|
||||
helper.assertTrue(BlockKnowledgeService.palette(player, BlockKnowledgeFacet.CARRIED, natural).isEmpty(),
|
||||
"Current natural stock must be empty even while the player carries disallowed concrete");
|
||||
|
||||
var discovered = BlockKnowledgeService.palette(player,
|
||||
BlockKnowledgeFacet.SEEN.or(BlockKnowledgeFacet.POSSESSED), natural);
|
||||
helper.assertTrue(discovered.stream().map(BlockKnowledge::blockId).toList()
|
||||
.equals(List.of("minecraft:oak_log", "minecraft:stone")),
|
||||
"Composed evidence must produce a stable sorted palette without admitting unknown materials");
|
||||
helper.assertTrue(discovered.getFirst().seen() && !discovered.getFirst().possessed()
|
||||
&& discovered.getFirst().carried() == 0,
|
||||
"Seeing a log must not manufacture possession or a usable quantity");
|
||||
boolean immutable = false;
|
||||
try { discovered.clear(); }
|
||||
catch (UnsupportedOperationException expected) { immutable = true; }
|
||||
helper.assertTrue(immutable, "Consumers must receive an immutable material palette");
|
||||
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.STONE, 2));
|
||||
var available = BlockKnowledgeService.palette(player,
|
||||
BlockKnowledgeFacet.POSSESSED.and(BlockKnowledgeFacet.CARRIED), natural);
|
||||
helper.assertTrue(available.size() == 1 && available.getFirst().blockId().equals("minecraft:stone")
|
||||
&& available.getFirst().carried() == 2,
|
||||
"An available-material palette must report only the exact current allowed quantity");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private record Notice(BlockKnowledgeEvents.Activity activity, BlockKnowledge knowledge) { }
|
||||
|
||||
private static ServerPlayer player(GameTestHelper helper) {
|
||||
ServerPlayer player = (ServerPlayer) helper.makeMockServerPlayer(GameType.SURVIVAL);
|
||||
player.setPos(helper.absoluteVec(new Vec3(5.5, 3, 5.5)));
|
||||
return player;
|
||||
}
|
||||
|
||||
private static boolean place(ServerPlayer player, Item item, BlockPos support, Direction face) {
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(item));
|
||||
var hit = new BlockHitResult(Vec3.atCenterOf(support), face, support, false);
|
||||
var context = new BlockPlaceContext(new UseOnContext(player, InteractionHand.MAIN_HAND, hit));
|
||||
return ((BlockItem) item).place(context).consumesAction();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
-117
@@ -1,117 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Bounded comparison of the real registered density, followed by inspection of ordinary FULL chunks. */
|
||||
public final class Caves28WorldGameTests {
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void geologyAndNativeOres(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var binding = generator.starterContext();
|
||||
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
|
||||
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
|
||||
helper.assertTrue(field.geology28(), "Current world uses the native-cave adaptation");
|
||||
var current = binding.random().getSampler(field);
|
||||
var previous = binding.random().getSampler(field.legacy27());
|
||||
var context = SamplerContext.builder().enableCaches().build();
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("scope", "Registered density comparison; no synthetic terrain, water, ore or room is placed by the test");
|
||||
var heights = new ArrayList<List<Integer>>();
|
||||
long oldSolid = 0, remainingSolid = 0, excavated = 0, restored = 0;
|
||||
int min = -Math.min(320, generator.initialDiameter() / 2), max = -min, raised = 0, lowered = 0, largestUplift = 0, largestSubsidence = 0;
|
||||
for (int z = min; z <= max; z += 32) for (int x = min; x <= max; x += 32) {
|
||||
int oldTop = 0, newTop = 0;
|
||||
for (int y = 2; y <= 382; y += 2) {
|
||||
boolean old = previous.sampleValue(context, x, y, z) > 0;
|
||||
boolean now = current.sampleValue(context, x, y, z) > 0;
|
||||
if (old) oldTop = y; if (now) newTop = y;
|
||||
if (y >= 48 && y <= 202) {
|
||||
if (old) { oldSolid++; if (now) remainingSolid++; else excavated++; }
|
||||
else if (now) restored++;
|
||||
}
|
||||
if (Math.hypot(x, z) < 40 && y >= 240)
|
||||
helper.assertTrue(old == now, "High arrival neighbourhood remains unchanged");
|
||||
}
|
||||
if (newTop > oldTop && oldTop > 208) { raised++; largestUplift = Math.max(largestUplift, newTop - oldTop); }
|
||||
if (newTop < oldTop && oldTop > 240) { lowered++; largestSubsidence = Math.max(largestSubsidence, oldTop - newTop); }
|
||||
heights.add(List.of(x, z, oldTop, newTop));
|
||||
}
|
||||
report.put("heights", heights); report.put("old_solid_samples", oldSolid);
|
||||
report.put("remaining_solid_samples", remainingSolid); report.put("excavated_samples", excavated);
|
||||
report.put("restored_rift_samples", restored); report.put("raised_columns", raised);
|
||||
report.put("largest_uplift", largestUplift);
|
||||
report.put("lowered_columns", lowered); report.put("largest_subsidence", largestSubsidence);
|
||||
helper.assertTrue(excavated > 0 && remainingSolid > excavated, "New passages exist while most sampled former rock remains");
|
||||
helper.assertTrue(raised > 0, "Native volumetric deformation raises some uplands");
|
||||
var childField = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
var child = new fr.koka.sanctuary.expansion.ExpansionIsland("geology_witness", "sanctuary", "east", "temperate",
|
||||
512, 2048, 0, level.getSeed(), "natural", "ready", 24);
|
||||
var childSettings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"))).value();
|
||||
var childState = net.minecraft.world.level.levelgen.RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), child.seed(), childSettings);
|
||||
var childNew = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField, child));
|
||||
var childOld = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField.legacy27(), child));
|
||||
long childExcavated = 0, childRemaining = 0;
|
||||
for (int z = -160; z <= 160; z += 32) for (int dx = -160; dx <= 160; dx += 32)
|
||||
for (int y = 48; y <= 202; y += 4) {
|
||||
if (childOld.sampleValue(context, 2048 + dx, y, z) > 0) {
|
||||
if (childNew.sampleValue(context, 2048 + dx, y, z) <= 0) childExcavated++;
|
||||
else childRemaining++;
|
||||
}
|
||||
}
|
||||
report.put("expansion_density", Map.of("diameter", 512, "excavated", childExcavated,
|
||||
"remaining", childRemaining, "reserved_or_opened", false));
|
||||
helper.assertTrue(childExcavated > 0 && childRemaining > childExcavated, "The actual expansion wrapper uses the same finite cave field");
|
||||
var sections = new LinkedHashMap<String, Object>();
|
||||
for (int z : new int[]{0, 96}) {
|
||||
var before = new ArrayList<String>(); var after = new ArrayList<String>();
|
||||
for (int y = 382; y >= 0; y -= 2) {
|
||||
var a = new StringBuilder(); var b = new StringBuilder();
|
||||
for (int x = min; x <= max; x += 2) {
|
||||
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
|
||||
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
|
||||
}
|
||||
before.add(a.toString()); after.add(b.toString());
|
||||
}
|
||||
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", min, "step", 2));
|
||||
}
|
||||
report.put("sections", sections);
|
||||
var ores = new TreeMap<String, Integer>(); var oreDepths = new TreeMap<String, List<Integer>>();
|
||||
for (int cx : new int[]{-6, 0, 6}) for (int cz : new int[]{-6, 0, 6}) {
|
||||
var chunk = level.getChunk(cx, cz, ChunkStatus.FULL, true);
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 1; y < 383; y++) {
|
||||
var state = chunk.getBlockState(new BlockPos(cx * 16 + x, y, cz * 16 + z));
|
||||
String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if (id.endsWith("_ore")) {
|
||||
ores.merge(id, 1, Integer::sum);
|
||||
var band = oreDepths.computeIfAbsent(id, ignored -> new ArrayList<>(List.of(384, 0)));
|
||||
band.set(0, Math.min(band.get(0), y)); band.set(1, Math.max(band.get(1), y));
|
||||
}
|
||||
}
|
||||
}
|
||||
report.put("full_chunks_inspected", 9); report.put("ores", ores); report.put("ore_y_ranges", oreDepths);
|
||||
helper.assertTrue(!ores.isEmpty(), "Actual generated rock contains native ore deposits");
|
||||
report.put("passed", true);
|
||||
Path path = Path.of("diagnostics/alpha28/geology.json"); Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.knowledge.census.LoadedChunks;
|
||||
import fr.koka.sanctuary.knowledge.census.TerrainCensus;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Native material changes and the non-loading contract of the live terrain census. */
|
||||
public final class CensusGameTestsTerrain {
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void chunkCensusRecountsPlacementReplacementAndRemoval(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
BlockPos pos = helper.absolutePos(new BlockPos(2, 3, 2));
|
||||
ChunkPos chunkPos = new ChunkPos(pos.getX() >> 4, pos.getZ() >> 4);
|
||||
var original = level.getBlockState(pos);
|
||||
long[] baseline = new long[2];
|
||||
TerrainCensus.ChunkSnapshot[] immutableBefore = new TerrainCensus.ChunkSnapshot[1];
|
||||
String pink = "minecraft:pink_glazed_terracotta";
|
||||
String orange = "minecraft:orange_glazed_terracotta";
|
||||
|
||||
helper.startSequence()
|
||||
.thenWaitUntil(() -> helper.assertTrue(current(helper, chunkPos).complete(),
|
||||
"The fixture chunk must finish its bounded initial material census"))
|
||||
.thenExecute(() -> {
|
||||
var snapshot = current(helper, chunkPos);
|
||||
baseline[0] = snapshot.blockCounts().getOrDefault(pink, 0L);
|
||||
baseline[1] = snapshot.blockCounts().getOrDefault(orange, 0L);
|
||||
immutableBefore[0] = snapshot;
|
||||
level.setBlockAndUpdate(pos, Blocks.GLAZED_TERRACOTTA.pick(DyeColor.PINK).defaultBlockState());
|
||||
})
|
||||
.thenWaitUntil(() -> {
|
||||
var placed = current(helper, chunkPos);
|
||||
helper.assertTrue(placed.complete() && placed.blockCounts().getOrDefault(pink, 0L) == baseline[0] + 1,
|
||||
"A native placement must add exactly one material to its loaded chunk");
|
||||
helper.assertTrue(immutableBefore[0].blockCounts().getOrDefault(pink, 0L) == baseline[0],
|
||||
"A previously returned census must not mutate when terrain changes");
|
||||
})
|
||||
.thenExecute(() -> {
|
||||
level.setBlockAndUpdate(pos, Blocks.GLAZED_TERRACOTTA.pick(DyeColor.ORANGE).defaultBlockState());
|
||||
var pending = current(helper, chunkPos);
|
||||
helper.assertTrue(pending.blockCounts().getOrDefault(pink, 0L) <= baseline[0],
|
||||
"A dirty section must stop contributing its obsolete material count immediately");
|
||||
helper.assertTrue(pending.countedSections() <= pending.loadedSections()
|
||||
&& pending.coverage() >= 0 && pending.coverage() <= 1,
|
||||
"Coverage must honestly describe the sections contributing to this snapshot");
|
||||
})
|
||||
.thenWaitUntil(() -> {
|
||||
var replaced = current(helper, chunkPos);
|
||||
helper.assertTrue(replaced.complete() && replaced.blockCounts().getOrDefault(pink, 0L) == baseline[0]
|
||||
&& replaced.blockCounts().getOrDefault(orange, 0L) == baseline[1] + 1,
|
||||
"Recounting must remove the replaced material and count its replacement once");
|
||||
})
|
||||
.thenExecute(() -> level.setBlockAndUpdate(pos, original))
|
||||
.thenWaitUntil(() -> {
|
||||
var removed = current(helper, chunkPos);
|
||||
helper.assertTrue(removed.complete() && removed.blockCounts().getOrDefault(pink, 0L) == baseline[0]
|
||||
&& removed.blockCounts().getOrDefault(orange, 0L) == baseline[1],
|
||||
"Removing the placed material must restore its original material totals");
|
||||
helper.assertTrue(removed.dimensionId().equals(level.dimension().identifier().toString())
|
||||
&& removed.chunkX() == chunkPos.x() && removed.chunkZ() == chunkPos.z(),
|
||||
"Spatial snapshots must retain their stable dimension and chunk coordinates");
|
||||
}).thenSucceed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void missingChunksStayUnloadedAndRepeatedReadsShareBudget(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var server = level.getServer();
|
||||
ChunkPos absent = new ChunkPos(1_000_000, 1_000_000);
|
||||
helper.assertTrue(level.getChunkSource().getChunkNow(absent.x(), absent.z()) == null,
|
||||
"The distant fixture must begin unloaded");
|
||||
int holdersBefore = level.getChunkSource().getLoadedChunksCount();
|
||||
var first = TerrainCensus.snapshot(server);
|
||||
int countedBefore = first.dimensions().stream().mapToInt(TerrainCensus.DimensionSnapshot::countedSections).sum();
|
||||
for (int read = 0; read < 32; read++) {
|
||||
helper.assertTrue(TerrainCensus.chunkSnapshot(level, absent).isEmpty(),
|
||||
"Requesting spatial metadata for a missing chunk must return absence");
|
||||
}
|
||||
var repeated = TerrainCensus.snapshot(server);
|
||||
helper.assertTrue(repeated.dimensions().stream().mapToInt(TerrainCensus.DimensionSnapshot::countedSections).sum() == countedBefore,
|
||||
"Repeated reads in the same tick must not spend another palette scan budget");
|
||||
helper.assertTrue(level.getChunkSource().getLoadedChunksCount() == holdersBefore
|
||||
&& level.getChunkSource().getChunkNow(absent.x(), absent.z()) == null,
|
||||
"Census reads must neither create holders nor load or generate distant chunks");
|
||||
var dimension = repeated.dimensions().stream()
|
||||
.filter(value -> value.dimensionId().equals(level.dimension().identifier().toString())).findFirst().orElseThrow();
|
||||
helper.assertTrue(dimension.loadedChunks() == LoadedChunks.full(level).size(),
|
||||
"Coverage must count only currently available FULL chunks, excluding unloaded terrain");
|
||||
helper.assertTrue(!dimension.blockCounts().containsKey("minecraft:air")
|
||||
&& !dimension.blockCounts().containsKey("minecraft:cave_air")
|
||||
&& !dimension.blockCounts().containsKey("minecraft:void_air"),
|
||||
"Terrain materials must not count empty space as available matter");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static TerrainCensus.ChunkSnapshot current(GameTestHelper helper, ChunkPos pos) {
|
||||
var result = TerrainCensus.chunkSnapshot(helper.getLevel(), pos);
|
||||
helper.assertTrue(result.isPresent(), "The active GameTest fixture chunk must already be loaded FULL");
|
||||
return result.orElseThrow();
|
||||
}
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.knowledge.census.StockCensus;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.CompoundContainer;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.vehicle.minecart.MinecartChest;
|
||||
import net.minecraft.world.inventory.ChestMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.HopperBlockEntity;
|
||||
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native fixtures: a census must not move items, invent stock or open unopened loot. */
|
||||
public final class CensusStockGameTests {
|
||||
@GameTest(maxTicks = 100)
|
||||
public void categoriesRemainSeparateAndOpenDoubleChestIsCountedOnce(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var server = level.getServer();
|
||||
var before = StockCensus.snapshot(server);
|
||||
BlockPos leftPos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
BlockPos rightPos = leftPos.east();
|
||||
BlockPos hopperPos = helper.absolutePos(new BlockPos(4, 1, 1));
|
||||
var state = Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING, Direction.NORTH);
|
||||
level.setBlockAndUpdate(leftPos, state.setValue(ChestBlock.TYPE, ChestType.LEFT));
|
||||
level.setBlockAndUpdate(rightPos, state.setValue(ChestBlock.TYPE, ChestType.RIGHT));
|
||||
level.setBlockAndUpdate(hopperPos, Blocks.HOPPER.defaultBlockState());
|
||||
var left = (ChestBlockEntity) level.getBlockEntity(leftPos);
|
||||
var right = (ChestBlockEntity) level.getBlockEntity(rightPos);
|
||||
var hopper = (HopperBlockEntity) level.getBlockEntity(hopperPos);
|
||||
left.setItem(0, new ItemStack(Items.GOLD_INGOT, 3));
|
||||
right.setItem(0, new ItemStack(Items.GOLD_INGOT, 5));
|
||||
hopper.setItem(0, new ItemStack(Items.GOLD_INGOT, 7));
|
||||
|
||||
ServerPlayer player = (ServerPlayer) helper.makeMockServerPlayer(GameType.SURVIVAL);
|
||||
player.getInventory().setItem(0, new ItemStack(Items.GOLD_INGOT, 6));
|
||||
player.setItemSlot(EquipmentSlot.OFFHAND, new ItemStack(Items.GOLD_INGOT, 2));
|
||||
player.setItemSlot(EquipmentSlot.HEAD, new ItemStack(Items.CARVED_PUMPKIN));
|
||||
player.containerMenu = ChestMenu.sixRows(1, player.getInventory(), new CompoundContainer(left, right));
|
||||
player.containerMenu.setCarried(new ItemStack(Items.GOLD_INGOT, 3));
|
||||
helper.assertTrue(!server.getPlayerList().getPlayers().contains(player), "Mock player must start outside the online list");
|
||||
helper.assertTrue(StockCensus.snapshot(server).playerItems().equals(before.playerItems()),
|
||||
"An offline player's inventory must not enter loaded online stock");
|
||||
|
||||
Vec3 position = helper.absoluteVec(new Vec3(3.5, 2, 3.5));
|
||||
var cart = new MinecartChest(EntityTypes.CHEST_MINECART, level);
|
||||
cart.setPos(position);
|
||||
cart.setItem(0, new ItemStack(Items.GOLD_INGOT, 11));
|
||||
level.addFreshEntity(cart);
|
||||
var item = new ItemEntity(level, position.x, position.y, position.z, new ItemStack(Items.GOLD_INGOT, 13));
|
||||
level.addFreshEntity(item);
|
||||
server.getPlayerList().getPlayers().add(player);
|
||||
try {
|
||||
var census = StockCensus.snapshot(server);
|
||||
String gold = "minecraft:gold_ingot";
|
||||
helper.assertTrue(census.playerItems().getOrDefault(gold, 0L) - before.playerItems().getOrDefault(gold, 0L) == 11,
|
||||
"Personal stock must include main inventory, equipment and cursor without the open chest");
|
||||
helper.assertTrue(census.blockContainerItems().getOrDefault(gold, 0L) - before.blockContainerItems().getOrDefault(gold, 0L) == 15,
|
||||
"Two chest halves and a hopper must each be counted once");
|
||||
helper.assertTrue(census.entityContainerItems().getOrDefault(gold, 0L) - before.entityContainerItems().getOrDefault(gold, 0L) == 11,
|
||||
"Chest minecart stock belongs to its own category");
|
||||
helper.assertTrue(census.groundItems().getOrDefault(gold, 0L) - before.groundItems().getOrDefault(gold, 0L) == 13,
|
||||
"Dropped stack units belong only to ground stock");
|
||||
helper.assertTrue(census.total(gold) - before.total(gold) == 50, "All four categories must total 50 real items");
|
||||
helper.assertTrue(census.playerItems().getOrDefault("minecraft:carved_pumpkin", 0L)
|
||||
- before.playerItems().getOrDefault("minecraft:carved_pumpkin", 0L) == 1,
|
||||
"Head equipment must be included exactly once");
|
||||
helper.assertTrue(left.getItem(0).getCount() == 3 && right.getItem(0).getCount() == 5 && hopper.getItem(0).getCount() == 7,
|
||||
"A stock census must preserve all container contents");
|
||||
helper.assertTrue(census.coverage().partial() && !census.coverage().offlineIncluded()
|
||||
&& !census.coverage().nestedContentsIncluded() && !census.coverage().mobInventoriesIncluded(),
|
||||
"Coverage must explicitly distinguish this loaded census from total world wealth");
|
||||
boolean immutable = false;
|
||||
try { census.playerItems().clear(); } catch (UnsupportedOperationException expected) { immutable = true; }
|
||||
helper.assertTrue(immutable, "Consumers must receive immutable stock maps");
|
||||
} finally {
|
||||
server.getPlayerList().getPlayers().remove(player);
|
||||
cart.discard();
|
||||
item.discard();
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100)
|
||||
public void unopenedBlockAndVehicleLootRemainUnresolved(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var server = level.getServer();
|
||||
var before = StockCensus.snapshot(server);
|
||||
BlockPos chestPos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
level.setBlockAndUpdate(chestPos, Blocks.CHEST.defaultBlockState());
|
||||
var chest = (ChestBlockEntity) level.getBlockEntity(chestPos);
|
||||
chest.setLootTable(BuiltInLootTables.SIMPLE_DUNGEON, 41L);
|
||||
|
||||
var cart = new MinecartChest(EntityTypes.CHEST_MINECART, level);
|
||||
cart.setPos(helper.absoluteVec(new Vec3(3.5, 2, 3.5)));
|
||||
cart.setContainerLootTable(BuiltInLootTables.ABANDONED_MINESHAFT);
|
||||
cart.setContainerLootTableSeed(73L);
|
||||
level.addFreshEntity(cart);
|
||||
try {
|
||||
var census = StockCensus.snapshot(server);
|
||||
helper.assertTrue(chest.getLootTable().equals(BuiltInLootTables.SIMPLE_DUNGEON) && chest.getLootTableSeed() == 41L,
|
||||
"Reading stock must preserve the chest's unopened loot table and seed");
|
||||
helper.assertTrue(cart.getContainerLootTable().equals(BuiltInLootTables.ABANDONED_MINESHAFT)
|
||||
&& cart.getContainerLootTableSeed() == 73L,
|
||||
"Reading stock must also preserve a minecart's unopened loot table and seed");
|
||||
helper.assertTrue(census.coverage().pendingLootContainers() - before.coverage().pendingLootContainers() == 2,
|
||||
"Both unopened containers must be explicitly reported as omitted loot");
|
||||
helper.assertTrue(census.blockContainerItems().equals(before.blockContainerItems())
|
||||
&& census.entityContainerItems().equals(before.entityContainerItems()),
|
||||
"An unresolved loot table must not manufacture counted item stacks");
|
||||
} finally {
|
||||
// Prevent cleanup from generating loot after the assertions.
|
||||
cart.setContainerLootTable(null);
|
||||
cart.discard();
|
||||
chest.setLootTable(null);
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.MainIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Test-only sampled density plots, not screenshots or a substitute for looking at generated chunks. */
|
||||
final class DensityDiagnostics {
|
||||
private static final int EXTENT = 304;
|
||||
private static final int STEP = 4;
|
||||
private static final int GRID = 2 * EXTENT / STEP + 1;
|
||||
private static final int MAP_SIZE = GRID * 4;
|
||||
private static final int VOID_COLOR = 0x132132;
|
||||
|
||||
private DensityDiagnostics() {}
|
||||
|
||||
static void verifyAndExport(GameTestHelper helper, LevelData.RespawnData spawn) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var registries = level.registryAccess();
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(SanctuarySpawn.RAISED_SETTINGS).value();
|
||||
DensityFunction finalDensity = registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("final_density_384"))).value();
|
||||
helper.assertTrue(finalDensity instanceof MainIslandDensity,
|
||||
"The diagnostic must inspect the actual production island density wrapper");
|
||||
MainIslandDensity island = (MainIslandDensity) finalDensity;
|
||||
DensityFunction legacy = registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("final_density"))).value();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
// The default run exports all three seeds once. Additional complete-world runs only
|
||||
// repeat the sparse assertions, avoiding redundant image generation.
|
||||
boolean export = level.getSeed() == 0L && Boolean.parseBoolean(System.getProperty("sanctuary.test.densityMaps", "true"));
|
||||
boolean foundUnforcedCentralVoid = false;
|
||||
for (long seed : new long[]{0L, 42L, 8675309L}) {
|
||||
RandomState state = RandomState.create(noises, seed, settings);
|
||||
boolean foundSolid = false;
|
||||
var bottomHeights = new java.util.TreeSet<Integer>();
|
||||
for (int x = -288; x <= 288; x += 16) {
|
||||
for (int z = -288; z <= 288; z += 16) {
|
||||
int firstSolid = -1;
|
||||
for (int y = 4; y < island.height(); y += 8) {
|
||||
float shaped = state.sampleBlockValueUncached(finalDensity, x, y, z);
|
||||
float natural = state.sampleBlockValueUncached(island.terrain(), x, y - island.verticalOffset(), z);
|
||||
helper.assertTrue(Float.isFinite(shaped), "Sculpted density must be finite");
|
||||
helper.assertTrue(shaped <= 0.0F || natural > 0.0F,
|
||||
"Sculpt must not invent solid terrain; seed " + seed + " at " + x + "," + y + "," + z);
|
||||
foundSolid |= shaped > 0.0F;
|
||||
if (shaped > 0.0F && firstSolid < 0) firstSolid = y;
|
||||
if (Math.abs(x) <= 48 && Math.abs(z) <= 48 && y >= 44 + island.verticalOffset() && y <= 108 + island.verticalOffset()) {
|
||||
foundUnforcedCentralVoid |= natural < 0.0F && shaped < 0.0F;
|
||||
}
|
||||
}
|
||||
if (firstSolid >= 0 && Math.abs(x) <= 96 && Math.abs(z) <= 96) bottomHeights.add(firstSolid);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(foundSolid, "The sampled natural island must contain land for seed " + seed);
|
||||
helper.assertTrue(bottomHeights.size() >= 4 && bottomHeights.first() >= 16,
|
||||
"The central underside must vary in height and leave an empty margin below; seed " + seed);
|
||||
SanctuaryMod.LOGGER.info("Raised island seed {}: sampled central underside Y{}..{} ({} distinct heights)",
|
||||
seed, bottomHeights.first(), bottomHeights.last(), bottomHeights.size());
|
||||
verifyUpperTranslation(helper, state, island, legacy, seed);
|
||||
if (export) {
|
||||
export(state, island, legacy, seed, seed == level.getSeed() ? spawn : null);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(foundUnforcedCentralVoid,
|
||||
"The regression seeds must exercise natural holes inside the former forced core");
|
||||
}
|
||||
|
||||
private static void verifyUpperTranslation(GameTestHelper helper, RandomState state,
|
||||
MainIslandDensity island, DensityFunction legacy, long seed) {
|
||||
for (int x = -256; x <= 256; x += 32) {
|
||||
for (int z = -256; z <= 256; z += 32) {
|
||||
for (int oldY = 80; oldY < 256; oldY += 8) {
|
||||
float before = state.sampleBlockValueUncached(legacy, x, oldY, z);
|
||||
float after = state.sampleBlockValueUncached(island, x, oldY + island.verticalOffset(), z);
|
||||
helper.assertTrue(Math.abs(before - after) < 0.000001F,
|
||||
"The upper silhouette must keep its seed's alpha.2 profile after translation; seed "
|
||||
+ seed + " at " + x + "," + oldY + "," + z + ": " + before + " -> " + after);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void export(RandomState state, MainIslandDensity island, DensityFunction legacy, long seed,
|
||||
LevelData.RespawnData spawn) throws IOException {
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
BufferedImage picture = new BufferedImage(MAP_SIZE * 2 + 60, 715 + 2 * (island.height() + 28), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = picture.createGraphics();
|
||||
graphics.setColor(new Color(0xF0F0EC));
|
||||
graphics.fillRect(0, 0, picture.getWidth(), picture.getHeight());
|
||||
graphics.setColor(new Color(0x14202A));
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 18));
|
||||
graphics.drawString("Seed " + seed + " | alpha.2 (actual preserved density)", 20, 27);
|
||||
graphics.drawString("Raised production density (384 blocks)", MAP_SIZE + 40, 27);
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
graphics.drawString("Top view: x/z -304..304, sample every 4 blocks; height sampled every 4 blocks", 20, 48);
|
||||
graphics.drawString("Dark = void; low green to high cream. Terrain only; no trees/materials.", MAP_SIZE + 40, 48);
|
||||
StringBuilder heights = new StringBuilder("x,z,alpha2_top_y,raised_top_y\n");
|
||||
int beforeColumns = 0;
|
||||
int afterColumns = 0;
|
||||
for (int gx = 0; gx < GRID; gx++) {
|
||||
int x = -EXTENT + gx * STEP;
|
||||
for (int gz = 0; gz < GRID; gz++) {
|
||||
int z = -EXTENT + gz * STEP;
|
||||
int beforeTop = -1;
|
||||
int afterTop = -1;
|
||||
// Stop as soon as both topmost samples have been found.
|
||||
for (int y = island.height() - 4; y >= 4 && (beforeTop < 0 || afterTop < 0); y -= 4) {
|
||||
if (beforeTop < 0) {
|
||||
if (state.sampleBlockValueUncached(legacy, x, y, z) > 0.0F) beforeTop = y;
|
||||
}
|
||||
if (afterTop < 0 && state.sampleBlockValueUncached(island, x, y, z) > 0.0F) afterTop = y;
|
||||
}
|
||||
beforeColumns += beforeTop >= 0 ? 1 : 0;
|
||||
afterColumns += afterTop >= 0 ? 1 : 0;
|
||||
graphics.setColor(new Color(heightColor(beforeTop)));
|
||||
graphics.fillRect(20 + gx * 4, 60 + gz * 4, 4, 4);
|
||||
graphics.setColor(new Color(heightColor(afterTop)));
|
||||
graphics.fillRect(MAP_SIZE + 40 + gx * 4, 60 + gz * 4, 4, 4);
|
||||
heights.append(x).append(',').append(z).append(',').append(beforeTop).append(',').append(afterTop).append('\n');
|
||||
}
|
||||
}
|
||||
if (spawn != null) {
|
||||
graphics.setColor(Color.MAGENTA);
|
||||
int px = MAP_SIZE + 40 + spawn.pos().getX() + EXTENT;
|
||||
int pz = 60 + spawn.pos().getZ() + EXTENT;
|
||||
graphics.drawOval(px - 4, pz - 4, 8, 8);
|
||||
graphics.drawString("Initial spawn " + spawn.pos().toShortString(), MAP_SIZE + 40, 689);
|
||||
}
|
||||
drawSections(graphics, state, island, legacy, false, 715);
|
||||
drawSections(graphics, state, island, legacy, true, 715 + island.height() + 28);
|
||||
graphics.dispose();
|
||||
Path output = directory.resolve("island-density-seed-" + seed + ".png");
|
||||
ImageIO.write(picture, "png", output.toFile());
|
||||
Files.writeString(directory.resolve("island-heightmap-seed-" + seed + ".csv"), heights);
|
||||
SanctuaryMod.LOGGER.info("Island density diagnostic seed {}: {} -> {} sampled land columns; {}",
|
||||
seed, beforeColumns, afterColumns, output.toAbsolutePath());
|
||||
}
|
||||
|
||||
private static void drawSections(Graphics2D graphics, RandomState state, MainIslandDensity island, DensityFunction legacy,
|
||||
boolean alongZ, int top) {
|
||||
graphics.setColor(new Color(0x14202A));
|
||||
graphics.drawString(alongZ ? "Section x=0: z -304..304; y 383 (top)..0 (bottom)"
|
||||
: "Section z=0: x -304..304; y 383 (top)..0 (bottom)", 20, top - 8);
|
||||
for (int horizontal = -EXTENT; horizontal < EXTENT; horizontal += 2) {
|
||||
int x = alongZ ? 0 : horizontal;
|
||||
int z = alongZ ? horizontal : 0;
|
||||
for (int y = 0; y < island.height(); y++) {
|
||||
float before = state.sampleBlockValueUncached(legacy, x, y, z);
|
||||
float after = state.sampleBlockValueUncached(island, x, y, z);
|
||||
graphics.setColor(new Color(before > 0.0F ? heightColor(y) : VOID_COLOR));
|
||||
graphics.fillRect(20 + horizontal + EXTENT, top + island.height() - 1 - y, 2, 1);
|
||||
graphics.setColor(new Color(after > 0.0F ? heightColor(y) : VOID_COLOR));
|
||||
graphics.fillRect(MAP_SIZE + 40 + horizontal + EXTENT, top + island.height() - 1 - y, 2, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int heightColor(int y) {
|
||||
if (y < 0) return VOID_COLOR;
|
||||
float height = Math.clamp(y / 280.0F, 0.0F, 1.0F);
|
||||
int red = (int) (43 + 192 * height);
|
||||
int green = (int) (91 + 139 * height);
|
||||
int blue = (int) (70 + 117 * height);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
}
|
||||
-278
@@ -1,278 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.expansion.ExpansionAdmission;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionJournal;
|
||||
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.expansion.ExpansionVoidCertificate;
|
||||
import fr.koka.sanctuary.expansion.ExpansionVoidReuse;
|
||||
import fr.koka.sanctuary.expansion.NativeStructureMetadata23;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
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;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** The real public admission transaction and preparation queue, with the production thirty-second deadline. */
|
||||
public final class Expansion23AdmissionGameTests {
|
||||
private static final int DISTANCE = 2048;
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void createPreparePersistAndReload(GameTestHelper helper) throws Exception {
|
||||
helper.assertTrue(Boolean.getBoolean("sanctuary.test.admission23"), "Admission fixture must be explicitly enabled");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 23 && helper.getLevel().structureManager().shouldGenerateStructures(),
|
||||
"Admission fixture requires generation23 with native structures enabled");
|
||||
var scenario = new Scenario(helper, generator);
|
||||
if (scenario.reload) scenario.verifyReload(); else scenario.create();
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionRuntime.Session session;
|
||||
final boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||
final boolean reserveOnly = Boolean.getBoolean("sanctuary.test.admission23.reserveOnly");
|
||||
final int diameter = Integer.getInteger("sanctuary.test.admission23.diameter", 256);
|
||||
final String climate = System.getProperty("sanctuary.test.admission23.climate", "temperate");
|
||||
final String relief = System.getProperty("sanctuary.test.admission23.relief", "natural");
|
||||
final Path world, directory, journal, proof;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<Map<String, Object>> attempts = new ArrayList<>();
|
||||
ExpansionIsland reservation;
|
||||
List<String> captured;
|
||||
String certificate = "";
|
||||
long preparationStarted;
|
||||
boolean drained;
|
||||
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator;
|
||||
helper.assertTrue(!reload || !reserveOnly, "Reserve-only costs require a fresh JVM and disposable world, not reload mode");
|
||||
session = ExpansionRuntime.session(level);
|
||||
world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
directory = world.resolve("data").resolve(generator.journalDirectory());
|
||||
journal = directory.resolve(ExpansionJournal.FILE_NAME); proof = world.resolve("admission23-create.json");
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world.toString());
|
||||
report.put("world_seed", level.getSeed()); report.put("generation", generator.rootGeneration());
|
||||
report.put("deadline_seconds", 30); report.put("attempts", attempts);
|
||||
report.put("reserve_only", reserveOnly); report.put("requested_diameter", diameter);
|
||||
report.put("requested_climate", climate); report.put("requested_relief", relief);
|
||||
report.put("no_expansion_preview_before_create", true);
|
||||
}
|
||||
|
||||
void create() throws Exception {
|
||||
helper.assertTrue(!Files.exists(proof) && session.islands().size() == 1, "Use a new disposable world for the first admission pass");
|
||||
long remaining = ExpansionAdmission.searchDeadline() - System.nanoTime();
|
||||
helper.assertTrue(remaining > TimeUnit.SECONDS.toNanos(29) && remaining <= TimeUnit.SECONDS.toNanos(30),
|
||||
"Fixture uses the unchanged production30s deadline");
|
||||
byte[] untouchedJournal = Files.readAllBytes(journal);
|
||||
var initialIslands = session.islands();
|
||||
try {
|
||||
for (int draw = 0; draw < 3 && reservation == null; draw++) {
|
||||
String id = "admission_23_" + draw;
|
||||
captured = StructureCensusService.priorities(level);
|
||||
long started = System.nanoTime();
|
||||
try {
|
||||
reservation = session.create(id, "sanctuary", "east", climate, diameter, relief, DISTANCE);
|
||||
long elapsed = elapsedMillis(started);
|
||||
attempts.add(Map.of("id", id, "result", "reserved", "milliseconds", elapsed));
|
||||
helper.assertTrue(elapsed <= 31_000, "Real admission completes within its30s budget plus bounded clock/IO overhead");
|
||||
} catch (IllegalArgumentException | IOException refused) {
|
||||
attempts.add(Map.of("id", id, "result", String.valueOf(refused.getMessage()), "milliseconds", elapsedMillis(started)));
|
||||
helper.assertTrue(session.islands().equals(initialIslands) && Arrays.equals(untouchedJournal, Files.readAllBytes(journal)),
|
||||
"A refused request leaves no reservation or journal mutation");
|
||||
// A timeout, IO failure or other implementation error is not a new random draw.
|
||||
if (!"native_structure_unavailable".equals(refused.getMessage())) throw refused;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(reservation != null, "A real expansion is admitted within three bounded feature refusals: " + diameter + " " + climate + " " + relief);
|
||||
helper.assertTrue(reservation.status().equals("reserved") && session.islands().size() == 2,
|
||||
"The transaction durably reserves exactly one child before preparation");
|
||||
helper.assertTrue(reservation.structurePriorities().equals(captured), "The stored profile equals the snapshot taken before create");
|
||||
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reservation.id()), "Fresh distant terrain prepares without a void-reuse reload");
|
||||
certificate = certificate(reservation);
|
||||
verifyJournal(reservation);
|
||||
var snapshot = Files.readAllBytes(journal);
|
||||
var repeated = session.create(reservation.id(), "sanctuary", "east", climate, diameter, relief, DISTANCE);
|
||||
helper.assertTrue(repeated.equals(reservation) && Arrays.equals(snapshot, Files.readAllBytes(journal)) && session.islands().size() == 2,
|
||||
"Repeating create on the reserved ID does not reserve again or rewrite the profile");
|
||||
if (reserveOnly) {
|
||||
report.put("island", encode(reservation)); report.put("captured_priorities", captured);
|
||||
report.put("journal_sha256", ExpansionVoidCertificate.digest(Files.readString(journal)));
|
||||
report.put("certificate", certificate); report.put("certificate_required", !reservation.reuseToken().isEmpty());
|
||||
report.put("generator_codec", generatorCodec()); report.put("idempotent", true);
|
||||
report.put("native_guarantee_checked_by_session_create", true);
|
||||
report.put("full_generation_checked", false); report.put("passed", true);
|
||||
write("reserve-" + diameter + "-" + climate + "-" + relief);
|
||||
helper.succeed(); return;
|
||||
}
|
||||
preparationStarted = System.nanoTime();
|
||||
helper.runAfterDelay(5, this::awaitReady);
|
||||
} catch (Exception failure) { fail(failure); }
|
||||
}
|
||||
|
||||
void awaitReady() {
|
||||
try {
|
||||
var current = session.island(reservation.id());
|
||||
if (!current.status().equals("ready")) {
|
||||
helper.assertTrue(session.progress().matches(java.util.regex.Pattern.quote(reservation.id()) + ": \\d+/\\d+"),
|
||||
"Real preparation must keep progressing, not stop silently: " + session.progress());
|
||||
helper.runAfterDelay(5, this::awaitReady); return;
|
||||
}
|
||||
if (!drained) { drained = true; helper.runAfterDelay(5, this::awaitReady); return; }
|
||||
helper.assertTrue(current.equals(reservation.ready()), "Preparation changes only reserved→ready, preserving identity and priorities");
|
||||
verifyJournal(current);
|
||||
helper.assertTrue(certificate.equals(certificate(current)), "Preparation cannot rewrite the certified terrain identity");
|
||||
var starts = nativeStarts(current);
|
||||
helper.assertTrue(!starts.isEmpty(), "An admitted expansion really creates at least one registered native start");
|
||||
verifyCensus(current, starts);
|
||||
report.put("priorities_after_generation", StructureCensusService.priorities(level));
|
||||
byte[] readyJournal = Files.readAllBytes(journal);
|
||||
helper.assertTrue(session.create(current.id(), "sanctuary", "east", current.climate(), current.diameter(), current.relief(), DISTANCE).equals(current),
|
||||
"A ready ID stays idempotent even after census evidence has changed");
|
||||
helper.assertTrue(Arrays.equals(readyJournal, Files.readAllBytes(journal)), "Idempotence preserves exact journal bytes");
|
||||
StructureCensusService.flush(level.getServer());
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Persist actual FULL preparation and census before shutdown");
|
||||
report.put("preparation_milliseconds", elapsedMillis(preparationStarted));
|
||||
report.put("island", encode(current)); report.put("captured_priorities", captured);
|
||||
report.put("journal_sha256", ExpansionVoidCertificate.digest(Files.readString(journal)));
|
||||
report.put("certificate", certificate); report.put("certificate_required", !current.reuseToken().isEmpty());
|
||||
report.put("generator_codec", generatorCodec()); report.put("native_starts", tags(starts));
|
||||
report.put("native_count", starts.size()); report.put("census_verified", true); report.put("idempotent", true);
|
||||
report.put("passed", true); write("create");
|
||||
Files.writeString(proof, json()); helper.succeed();
|
||||
} catch (Exception failure) { fail(failure); }
|
||||
}
|
||||
|
||||
void verifyReload() throws Exception {
|
||||
try {
|
||||
var previous = JsonParser.parseString(Files.readString(proof)).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean(), "Reload requires a successful first admission pass");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use another JVM");
|
||||
helper.assertTrue(previous.get("world_directory").getAsString().equals(world.toString())
|
||||
&& previous.get("world_seed").getAsLong() == level.getSeed(), "Reload preserves the same development world and seed");
|
||||
var saved = ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, previous.get("island")).getOrThrow();
|
||||
var current = session.island(saved.id());
|
||||
helper.assertTrue(current.equals(saved) && current.status().equals("ready") && session.islands().size() == 2,
|
||||
"Startup restores the ready child directly, without another create call");
|
||||
helper.assertTrue(generator.source().islands().equals(session.islands()), "Loaded world generation uses the journal snapshot");
|
||||
helper.assertTrue(ExpansionVoidCertificate.digest(Files.readString(journal)).equals(previous.get("journal_sha256").getAsString()),
|
||||
"Opening a ready journal preserves its exact bytes");
|
||||
verifyJournal(current);
|
||||
helper.assertTrue(previous.get("generator_codec").equals(generatorCodec()), "The generator codec survives a full process restart");
|
||||
helper.assertTrue(previous.get("certificate").getAsString().equals(certificate(current)), "The same certificate decision survives startup");
|
||||
var starts = nativeStarts(current);
|
||||
helper.assertTrue(previous.get("native_starts").equals(tags(starts)), "Native start identity, contents and reference counters survive reload");
|
||||
verifyCensus(current, starts);
|
||||
helper.assertTrue(session.progress().equals("idle"), "Ready regions do not restart their preparation job");
|
||||
report.put("island", encode(current)); report.put("native_count", starts.size());
|
||||
report.put("previous_process_id", previous.get("process_id").getAsLong());
|
||||
report.put("create_called", false); report.put("census_verified", true); report.put("passed", true);
|
||||
write("reload"); helper.succeed();
|
||||
} catch (Exception failure) { fail(failure); }
|
||||
}
|
||||
|
||||
private List<StructureStart> nativeStarts(ExpansionIsland island) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var starts = new ArrayList<StructureStart>();
|
||||
for (var candidate : ExpansionStructures23.candidates(island)) {
|
||||
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
for (var start : chunk.getAllStarts().values()) {
|
||||
if (!start.isValid()) continue;
|
||||
var id = registry.getKey(start.getStructure());
|
||||
if (id != null && id.getNamespace().equals("minecraft")
|
||||
&& ((NativeStructureMetadata23) (Object) start).sanctuary$native23() != null) starts.add(start);
|
||||
}
|
||||
}
|
||||
starts.sort(Comparator.comparingInt((StructureStart s) -> s.getChunkPos().x()).thenComparingInt(s -> s.getChunkPos().z())
|
||||
.thenComparing(s -> registry.getKey(s.getStructure()).toString()));
|
||||
return List.copyOf(starts);
|
||||
}
|
||||
|
||||
private void verifyCensus(ExpansionIsland island, List<StructureStart> starts) {
|
||||
var view = StructureCensusService.snapshot(level.getServer());
|
||||
helper.assertTrue(view.available(), "Passive structure census stays available through actual generation and reload");
|
||||
var region = view.regions().stream().filter(r -> r.island().equals(island.id())).findFirst().orElseThrow();
|
||||
helper.assertTrue(region.coverage().startsComplete() && region.coverage().fullChunks() == region.coverage().expectedChunks(),
|
||||
"The preparation queue certifies finite native-start and FULL coverage of the ready island");
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (var start : starts) {
|
||||
String id = registry.getKey(start.getStructure()).toString();
|
||||
var key = new StructureCensusMemory.SiteKey(level.dimension().identifier().toString(), id,
|
||||
start.getChunkPos().x(), 0, start.getChunkPos().z(), false);
|
||||
var indexed = view.evidence().sites().get(key);
|
||||
helper.assertTrue(indexed != null && view.evidence().generated(indexed), "The census records the real generated occurrence: " + id);
|
||||
var family = region.families().get(StructureCensusService.family(id));
|
||||
helper.assertTrue(family != null && family.presence() == StructureCensusService.Presence.PRESENT && family.generated() > 0,
|
||||
"A generated native family is present, never absent or unknown");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyJournal(ExpansionIsland island) throws IOException {
|
||||
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, encode(island)).getOrThrow().equals(island), "Every descriptor field survives CODEC roundtrip");
|
||||
var reopened = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
|
||||
helper.assertTrue(reopened.islands().equals(session.islands()) && reopened.islands().contains(island),
|
||||
"A separate read of the durable journal agrees with the live session");
|
||||
}
|
||||
|
||||
private String certificate(ExpansionIsland island) throws IOException {
|
||||
if (island.reuseToken().isEmpty()) return ""; // Fully absent terrain needs no void-reuse certificate.
|
||||
String text = Files.readString(directory.resolve("void-reuse").resolve(island.reuseToken() + ".json"));
|
||||
helper.assertTrue(ExpansionVoidCertificate.digest(text).equals(island.reuseToken()), "Certificate content matches its immutable token");
|
||||
var body = JsonParser.parseString(text).getAsJsonObject();
|
||||
helper.assertTrue(ExpansionIsland.CODEC.parse(JsonOps.INSTANCE, body.get("island")).getOrThrow().equals(island.withReuseToken("").ready()),
|
||||
"Certificate binds the same immutable descriptor and priority profile");
|
||||
return text;
|
||||
}
|
||||
|
||||
private JsonArray tags(List<StructureStart> starts) {
|
||||
var result = new JsonArray(); var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
for (var start : starts) {
|
||||
var saved = new JsonObject(); saved.addProperty("id", registry.getKey(start.getStructure()).toString());
|
||||
saved.addProperty("x", start.getChunkPos().x()); saved.addProperty("z", start.getChunkPos().z());
|
||||
saved.addProperty("nbt", start.createTag(context, start.getChunkPos()).toString()); result.add(saved);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private com.google.gson.JsonElement encode(ExpansionIsland island) { return ExpansionIsland.CODEC.encodeStart(JsonOps.INSTANCE, island).getOrThrow(); }
|
||||
private com.google.gson.JsonElement generatorCodec() {
|
||||
return ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
|
||||
}
|
||||
private String json() { return new GsonBuilder().setPrettyPrinting().create().toJson(report); }
|
||||
private void write(String name) throws IOException {
|
||||
var output = Path.of("diagnostics", "admission23"); Files.createDirectories(output); Files.writeString(output.resolve(name + ".json"), json());
|
||||
}
|
||||
private void fail(Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString()); report.put("progress", session.progress());
|
||||
try { write("failure"); } catch (IOException ignored) {}
|
||||
throw new IllegalStateException("Production admission23 failed", failure);
|
||||
}
|
||||
}
|
||||
private static long elapsedMillis(long started) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started); }
|
||||
}
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import net.minecraft.commands.CommandSource;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ChunkLevel;
|
||||
import net.minecraft.server.permissions.LevelBasedPermissionSet;
|
||||
import net.minecraft.server.permissions.PermissionLevel;
|
||||
import net.minecraft.server.permissions.PermissionSet;
|
||||
|
||||
/** Tests real registered command permissions using sources; no real or simulated creative player is created. */
|
||||
public final class ExpansionCommandChecks {
|
||||
private ExpansionCommandChecks() { }
|
||||
|
||||
public static void verify(GameTestHelper helper) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var server = level.getServer();
|
||||
var session = ExpansionRuntime.session(level);
|
||||
var before = session.islands();
|
||||
var dispatcher = server.getCommands().getDispatcher();
|
||||
var console = server.createCommandSourceStack().withLevel(level).withSource(CommandSource.NULL).withSuppressedOutput();
|
||||
var levelZero = console.withPermission(LevelBasedPermissionSet.forLevel(PermissionLevel.ALL));
|
||||
var noPermissions = console.withPermission(PermissionSet.NO_PERMISSIONS);
|
||||
var levelOne = console.withPermission(LevelBasedPermissionSet.MODERATOR);
|
||||
var operatorTwo = console.withPermission(LevelBasedPermissionSet.GAMEMASTER);
|
||||
var admin = console.withPermission(LevelBasedPermissionSet.ADMIN);
|
||||
helper.assertTrue(console.getEntity() == null, "Command permission checks use console-derived sources, not a creative-player simulation");
|
||||
helper.assertTrue(ChunkLevel.RADIUS_AROUND_FULL_CHUNK * 16 <= ExpansionIsland.GENERATION_HALO,
|
||||
"The declared expansion halo must include the exact engine FULL generation dependency radius");
|
||||
|
||||
var sanctuary = dispatcher.getRoot().getChild("sanctuary");
|
||||
helper.assertTrue(sanctuary != null && sanctuary.getChild("expansion") != null,
|
||||
"The production Sanctuary expansion command tree must be registered");
|
||||
helper.assertTrue(!sanctuary.canUse(levelZero) && !sanctuary.canUse(noPermissions) && !sanctuary.canUse(levelOne),
|
||||
"Expansion commands must reject level 0, absent permissions and OP1 regardless of any game-mode assumption");
|
||||
helper.assertTrue(sanctuary.canUse(operatorTwo) && sanctuary.canUse(admin) && sanctuary.canUse(console),
|
||||
"Expansion commands must admit OP2 and higher, including the dedicated server console");
|
||||
|
||||
String create = "sanctuary expansion create command_denied sanctuary north boreal 64 natural";
|
||||
expectDenied(helper, dispatcher, levelZero, create, "level 0");
|
||||
expectDenied(helper, dispatcher, noPermissions, create, "no permissions");
|
||||
expectDenied(helper, dispatcher, levelOne, create, "OP1");
|
||||
helper.assertTrue(session.islands().equals(before), "Denied command sources must not add or change expansion regions");
|
||||
|
||||
String preview = "sanctuary expansion preview command_preview sanctuary north boreal 64 natural";
|
||||
try {
|
||||
var parsed = dispatcher.parse(preview, console);
|
||||
helper.assertTrue(!parsed.getReader().canRead() && parsed.getExceptions().isEmpty(),
|
||||
"The console must parse a complete preview with the known Sanctuary parent");
|
||||
helper.assertTrue(dispatcher.execute(parsed) == 1, "The console preview must execute successfully through the registered dispatcher");
|
||||
helper.assertTrue(dispatcher.execute(preview, operatorTwo) == 1, "OP2 must execute the same read-only preview successfully");
|
||||
helper.assertTrue(session.islands().equals(before), "Preview must not reserve or activate an island");
|
||||
|
||||
var feedback = new ArrayList<Component>();
|
||||
var previewSource = server.createCommandSourceStack().withLevel(level).withPermission(LevelBasedPermissionSet.GAMEMASTER)
|
||||
.withSource(new CommandSource() {
|
||||
@Override public void sendSystemMessage(Component message) { feedback.add(message); }
|
||||
@Override public boolean acceptsSuccess() { return true; }
|
||||
@Override public boolean acceptsFailure() { return true; }
|
||||
@Override public boolean shouldInformAdmins() { return false; }
|
||||
});
|
||||
var parent = session.island("sanctuary");
|
||||
for (String direction : new String[] {"north", "northeast"}) {
|
||||
feedback.clear();
|
||||
helper.assertTrue(dispatcher.execute("sanctuary expansion preview distance_preview sanctuary " + direction
|
||||
+ " boreal 64 natural 2048", previewSource) == 1, "The optional distance must execute for " + direction);
|
||||
helper.assertTrue(feedback.size() == 1 && feedback.getFirst().getContents() instanceof TranslatableContents,
|
||||
"A successful distance preview must return its proposed coordinates");
|
||||
var result = (TranslatableContents) feedback.getFirst().getContents();
|
||||
helper.assertTrue(result.getKey().equals("sanctuary.expansion.preview"), "Distance preview must return the production preview response");
|
||||
var args = result.getArgs();
|
||||
int x = ((Number) args[1]).intValue(), z = ((Number) args[2]).intValue();
|
||||
int dx = x - parent.centerX(), dz = z - parent.centerZ();
|
||||
helper.assertTrue(Math.hypot(dx, dz) >= 2048,
|
||||
"The command distance must be a minimum centre-to-centre distance, including diagonals");
|
||||
helper.assertTrue(dz < 0 && (direction.equals("north") ? dx == 0 : dx == -dz),
|
||||
"The distance preview must preserve the exact requested direction");
|
||||
helper.assertTrue(session.islands().equals(before), "Distance preview must not reserve or activate an island");
|
||||
}
|
||||
for (String command : new String[] {
|
||||
"sanctuary expansion quick north",
|
||||
"sanctuary expansion quick north boreal 64 natural",
|
||||
"sanctuary expansion quick north boreal 64 natural 2048",
|
||||
"sanctuary expansion create distance_create sanctuary north boreal 64 natural",
|
||||
"sanctuary expansion create distance_create sanctuary north boreal 64 natural 2048"}) {
|
||||
var syntax = dispatcher.parse(command, operatorTwo);
|
||||
helper.assertTrue(!syntax.getReader().canRead() && syntax.getExceptions().isEmpty() && syntax.getContext().getCommand() != null,
|
||||
"Old and distance-aware create/quick syntax must remain executable: " + command);
|
||||
}
|
||||
helper.assertTrue(dispatcher.execute(preview + " 0", operatorTwo) == 1,
|
||||
"An explicit zero distance must preserve automatic positioning");
|
||||
|
||||
for (String distance : new String[] {"-1", "100001"}) {
|
||||
for (String command : new String[] {
|
||||
preview,
|
||||
"sanctuary expansion create distance_invalid sanctuary north boreal 64 natural",
|
||||
"sanctuary expansion quick north boreal 64 natural"}) {
|
||||
boolean distanceRejected = false;
|
||||
try { dispatcher.execute(command + " " + distance, operatorTwo); }
|
||||
catch (CommandSyntaxException expected) { distanceRejected = true; }
|
||||
helper.assertTrue(distanceRejected && session.islands().equals(before),
|
||||
"All expansion commands must reject an out-of-range distance without mutation");
|
||||
}
|
||||
}
|
||||
|
||||
// 65 is accepted by Brigadier's numeric bounds, then rejected by the production size contract.
|
||||
int invalid = dispatcher.execute("sanctuary expansion create command_invalid sanctuary east boreal 65 natural", operatorTwo);
|
||||
helper.assertTrue(invalid == 0, "An unsupported nominal diameter must fail in the production command handler");
|
||||
helper.assertTrue(session.islands().equals(before), "Rejecting an unsupported diameter must preserve the registry");
|
||||
|
||||
boolean outsideRangeRejected = false;
|
||||
try { dispatcher.execute("sanctuary expansion create command_out_of_range sanctuary east boreal 1025 natural", console); }
|
||||
catch (CommandSyntaxException expected) { outsideRangeRejected = true; }
|
||||
helper.assertTrue(outsideRangeRejected && session.islands().equals(before),
|
||||
"The parser must reject a diameter above 1024 before changing the registry");
|
||||
} catch (CommandSyntaxException exception) {
|
||||
throw new IOException("Registered expansion command checks failed", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void expectDenied(GameTestHelper helper, CommandDispatcher<CommandSourceStack> dispatcher,
|
||||
CommandSourceStack source, String command, String label) {
|
||||
var parsed = dispatcher.parse(command, source);
|
||||
helper.assertTrue(parsed.getContext().getNodes().isEmpty() && parsed.getReader().canRead(),
|
||||
"The " + label + " source must not parse the restricted expansion command tree");
|
||||
boolean rejected = false;
|
||||
try { dispatcher.execute(parsed); }
|
||||
catch (CommandSyntaxException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "The " + label + " source must not execute an expansion command");
|
||||
}
|
||||
}
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka.sanctuary.expansion.ExpansionAdmission;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||
import fr.koka.sanctuary.expansion.ExpansionVoidReuse;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Opt-in, disposable-world proof. Run fresh, then twice with sanctuaryExpansionReload=true. */
|
||||
public final class ExpansionVoidReuseGameTests {
|
||||
private static final String LOADED = "void_loaded", SAVED = "void_saved";
|
||||
@GameTest(maxTicks = 6000)
|
||||
public void certifiedVoidRestartsThroughNativeGeneration(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"Void reuse proof needs the unified generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(ExpansionIsland.spatialGeneration(generator.rootGeneration()), "Void reuse is restricted to spatial city generations");
|
||||
new Scenario(helper, generator).advance();
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionRuntime.Session session;
|
||||
final boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
ExpansionIsland loaded, saved;
|
||||
final List<ExpansionIsland> rejectionFixtures = new ArrayList<>();
|
||||
int phase;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator;
|
||||
this.session = ExpansionRuntime.session(level);
|
||||
report.put("world_seed", level.getSeed()); report.put("generation", generator.rootGeneration());
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (reload) { resume(); return; }
|
||||
if (phase == 0) {
|
||||
loaded = session.candidate(LOADED, "sanctuary", "east", "temperate", 64, "natural", 6144);
|
||||
for (var pos : positions(loaded)) {
|
||||
level.setChunkForced(pos.x(), pos.z(), true);
|
||||
level.getChunk(pos.x(), pos.z());
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
var fixture = new ExpansionIsland("reject_" + i, "sanctuary", "north", "temperate", 64,
|
||||
0, -12288 - i * 4096, level.getSeed() + i, "natural", "reserved", generator.rootGeneration());
|
||||
rejectionFixtures.add(fixture);
|
||||
for (var pos : positions(fixture)) {
|
||||
level.setChunkForced(pos.x(), pos.z(), true);
|
||||
level.getChunk(pos.x(), pos.z());
|
||||
}
|
||||
}
|
||||
phase = 1;
|
||||
helper.runAtTickTime(helper.getTick() + 40, this::advance);
|
||||
return;
|
||||
}
|
||||
if (phase == 1) {
|
||||
report.put("stage", "loaded_positive_baseline");
|
||||
var proof = ExpansionVoidReuse.inspect(level, loaded, ExpansionAdmission.searchDeadline());
|
||||
helper.assertTrue(proof.certificates().stream().noneMatch(c -> c.absent()), "Loaded fixture needs native FULL void throughout its write scope");
|
||||
report.put("loaded_positive_baseline_passed", true);
|
||||
report.put("stage", "independent_rejections");
|
||||
rejectionChecks(helper, rejectionFixtures);
|
||||
report.put("stage", "loaded_positive_reservation");
|
||||
var source = level.getChunk(loaded.centerX() >> 4, loaded.centerZ() >> 4);
|
||||
CompoundTag snapshot = SerializableChunkData.copyOf(level, source).write();
|
||||
var admitted = session.create(LOADED, "sanctuary", "east", "temperate", 64, "natural", 6144);
|
||||
helper.assertTrue(admitted.centerX() == loaded.centerX() && admitted.centerZ() == loaded.centerZ(), "Inspection must retain the requested candidate");
|
||||
helper.assertTrue(!admitted.reuseToken().isEmpty() && ExpansionVoidReuse.requiresReload(level, LOADED), "Loaded FULL void must be deferred with a durable journal token");
|
||||
helper.assertTrue(session.island(LOADED).status().equals("reserved"), "A live FULL reservation cannot become ready before reopening");
|
||||
helper.assertTrue(generator.source().islands().stream().noneMatch(i -> i.id().equals(LOADED)), "Deferred island must remain invisible to generation");
|
||||
loaded = admitted;
|
||||
|
||||
report.put("stage", "saved_positive_baseline");
|
||||
saved = session.candidate(SAVED, "sanctuary", "west", "temperate", 64, "natural", 6144);
|
||||
var pos = new ChunkPos(saved.centerX() >> 4, saved.centerZ() >> 4);
|
||||
helper.assertTrue(level.getChunkSource().chunkMap.read(pos).get(30, TimeUnit.SECONDS).isEmpty(), "Saved-only fixture starts from absent storage");
|
||||
// One genuine native FULL-void snapshot, relocated only inside this disposable test fixture.
|
||||
snapshot.putInt("xPos", pos.x()); snapshot.putInt("zPos", pos.z());
|
||||
level.getChunkSource().chunkMap.write(pos, snapshot).get(30, TimeUnit.SECONDS);
|
||||
level.getChunkSource().chunkMap.synchronize(true).get(30, TimeUnit.SECONDS);
|
||||
var savedProof = ExpansionVoidReuse.inspect(level, saved, ExpansionAdmission.searchDeadline());
|
||||
helper.assertTrue(savedProof.certificates().stream().filter(c -> !c.absent()).count() == 1
|
||||
&& savedProof.certificates().stream().anyMatch(c -> c.absent()), "Saved-only fixture needs both FULL and certified absent positions");
|
||||
report.put("stage", "external_mutation_preflight");
|
||||
verifyMutationBlocksWholePreflight(helper, generator, savedProof, snapshot, pos);
|
||||
report.put("stage", "saved_positive_reservation");
|
||||
saved = session.create(SAVED, "sanctuary", "west", "temperate", 64, "natural", 6144);
|
||||
helper.assertTrue(!saved.reuseToken().isEmpty() && ExpansionVoidReuse.requiresReload(level, SAVED), "Saved void also needs a reload transaction");
|
||||
for (var loadedPos : positions(loaded)) level.setChunkForced(loadedPos.x(), loadedPos.z(), false);
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save native void and reservations before the first real restart");
|
||||
report.put("loaded_full_chunks", positions(loaded).size());
|
||||
report.put("saved_only_full_chunks", 1);
|
||||
report.put("saved_only_absent_chunks", savedProof.certificates().stream().filter(c -> c.absent()).count());
|
||||
report.put("loaded_token", loaded.reuseToken()); report.put("saved_token", saved.reuseToken());
|
||||
report.put("rejected_block_chest_entity_tick", true); report.put("foreign_mutation_preserved_region_bytes", true);
|
||||
report.put("passed", true); write(level, "void-reuse-before-reload.json", report);
|
||||
helper.succeed();
|
||||
}
|
||||
} catch (Exception error) {
|
||||
report.put("passed", false); report.put("failure", error.toString());
|
||||
try { write(level, "void-reuse-failure.json", report); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Native void reuse proof failed", error);
|
||||
}
|
||||
}
|
||||
void resume() throws Exception {
|
||||
loaded = session.island(LOADED); saved = session.island(SAVED);
|
||||
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, LOADED) && !ExpansionVoidReuse.requiresReload(level, SAVED),
|
||||
"A genuine restart must open durable transactions");
|
||||
if (!loaded.status().equals("ready") || !saved.status().equals("ready")) {
|
||||
helper.assertTrue(ExpansionVoidReuse.failure(level, LOADED) == null && ExpansionVoidReuse.failure(level, SAVED) == null,
|
||||
"Native load must not block either certified transaction");
|
||||
helper.runAtTickTime(helper.getTick() + 20, this::advance);
|
||||
return;
|
||||
}
|
||||
Path witnessFile = path(level, "void-reuse-after-reload.json");
|
||||
boolean secondReload = Files.exists(witnessFile);
|
||||
if (secondReload) {
|
||||
var original = JsonParser.parseString(Files.readString(witnessFile)).getAsJsonObject();
|
||||
var p = original.getAsJsonArray("gold_witness");
|
||||
helper.assertTrue(level.getBlockState(new BlockPos(p.get(0).getAsInt(), p.get(1).getAsInt(), p.get(2).getAsInt())).is(Blocks.GOLD_BLOCK),
|
||||
"A native marked FULL chunk must preserve gameplay blocks on subsequent reloads");
|
||||
}
|
||||
int full = 0, nonAir = 0, rebuiltBiomeSamples = 0;
|
||||
for (var island : List.of(loaded, saved)) for (var pos : positions(island)) {
|
||||
var chunk = level.getChunk(pos.x(), pos.z());
|
||||
helper.assertTrue(chunk.getPersistedStatus() == ChunkStatus.FULL && ExpansionVoidReuse.marker(chunk).equals(island.reuseToken()),
|
||||
"Every claimed position must complete the native pipeline and retain its exact marker: " + pos);
|
||||
helper.assertTrue(chunk.isLightCorrect(), "Every reconstructed FULL chunk must finish native lighting: " + pos);
|
||||
int midX = pos.x() * 16 + 8, midZ = pos.z() * 16 + 8;
|
||||
if (island.contains(midX, midZ)) {
|
||||
helper.assertTrue(!chunk.getNoiseBiome(pos.x() * 4 + 2, 220 / 4, pos.z() * 4 + 2)
|
||||
.is(net.minecraft.world.level.biome.Biomes.THE_VOID),
|
||||
"Native BIOMES must replace the previous void biome inside the expansion: " + pos);
|
||||
rebuiltBiomeSamples++;
|
||||
}
|
||||
full++;
|
||||
for (var section : chunk.getSections()) if (!section.hasOnlyAir()) nonAir++;
|
||||
}
|
||||
helper.assertTrue(nonAir > 0, "A reconstructed expansion must contain actual generated terrain");
|
||||
var witness = new BlockPos(loaded.centerX(), 376, loaded.centerZ());
|
||||
if (!secondReload) level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush generated native chunks and trusted markers");
|
||||
report.put("full_marked_chunks", full); report.put("nonempty_sections", nonAir);
|
||||
report.put("light_correct_chunks", full); report.put("rebuilt_biome_samples", rebuiltBiomeSamples);
|
||||
report.put("gold_witness", List.of(witness.getX(), witness.getY(), witness.getZ()));
|
||||
report.put("second_reload_preserved_gameplay", secondReload); report.put("passed", true);
|
||||
write(level, secondReload ? "void-reuse-second-reload.json" : "void-reuse-after-reload.json", report);
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
|
||||
private static void rejectionChecks(GameTestHelper helper, List<ExpansionIsland> fixtures) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
for (int i = 0; i < fixtures.size(); i++) {
|
||||
var island = fixtures.get(i); var p = new BlockPos(island.centerX(), 200, island.centerZ());
|
||||
// Each witness has its own untouched native palette. Never reuse a section edited by a prior witness.
|
||||
var baseline = ExpansionVoidReuse.inspect(level, island, ExpansionAdmission.searchDeadline());
|
||||
helper.assertTrue(baseline.certificates().stream().noneMatch(c -> c.absent()), "Every rejection first proves its own FULL-void baseline");
|
||||
switch (i) {
|
||||
case 0 -> {
|
||||
level.setBlock(p, Blocks.STONE.defaultBlockState(), 3);
|
||||
expectOccupied(helper, island, "block");
|
||||
}
|
||||
case 1 -> {
|
||||
level.setBlock(p, Blocks.CHEST.defaultBlockState(), 3);
|
||||
helper.assertTrue(level.getBlockEntity(p) != null, "Chest witness must contain its native block entity");
|
||||
expectOccupied(helper, island, "chest");
|
||||
}
|
||||
case 2 -> {
|
||||
var entity = EntityTypes.ZOMBIE.create(level, EntitySpawnReason.COMMAND);
|
||||
helper.assertTrue(entity != null, "Entity rejection needs a real entity");
|
||||
entity.setPos(p.getX() + .5, p.getY(), p.getZ() + .5);
|
||||
helper.assertTrue(level.addFreshEntity(entity), "Entity fixture must be registered with the native manager");
|
||||
expectOccupied(helper, island, "entity"); entity.discard();
|
||||
}
|
||||
case 3 -> {
|
||||
level.scheduleTick(p, Blocks.STONE, 1000);
|
||||
expectOccupied(helper, island, "tick");
|
||||
level.getBlockTicks().clearArea(new BoundingBox(p));
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected rejection witness");
|
||||
}
|
||||
for (var pos : positions(island)) level.setChunkForced(pos.x(), pos.z(), false);
|
||||
}
|
||||
}
|
||||
private static void expectOccupied(GameTestHelper helper, ExpansionIsland island, String kind) throws Exception {
|
||||
boolean refused = false;
|
||||
try { ExpansionVoidReuse.inspect(helper.getLevel(), island, ExpansionAdmission.searchDeadline()); }
|
||||
catch (ExpansionAdmission.OccupiedException expected) { refused = true; }
|
||||
helper.assertTrue(refused, "Admission must reject " + kind + " even in an otherwise empty void chunk");
|
||||
}
|
||||
|
||||
private static void verifyMutationBlocksWholePreflight(GameTestHelper helper, SanctuaryChunkGenerator generator,
|
||||
ExpansionVoidReuse.Inspection proof, CompoundTag original, ChunkPos pos) throws Exception {
|
||||
var level = helper.getLevel(); var session = ExpansionRuntime.session(level);
|
||||
String token = ExpansionVoidReuse.persist(proof);
|
||||
var mutated = original.copy(); mutated.putString("foreign_mod_metadata", "must survive unchanged");
|
||||
var storage = level.getChunkSource().chunkMap;
|
||||
storage.write(pos, mutated).get(30, TimeUnit.SECONDS); storage.synchronize(true).get(30, TimeUnit.SECONDS);
|
||||
Path region = net.minecraft.world.level.dimension.DimensionType.getStorageFolder(level.dimension(),
|
||||
level.getServer().getWorldPath(LevelResource.ROOT)).resolve("region/r."
|
||||
+ Math.floorDiv(pos.x(), 32) + "." + Math.floorDiv(pos.z(), 32) + ".mca");
|
||||
byte[] before = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(region));
|
||||
var islands = List.of(session.island("sanctuary"), proof.island().withReuseToken(token));
|
||||
boolean refused = false;
|
||||
try { ExpansionVoidReuse.open(level, generator, islands); } catch (IOException expected) { refused = true; }
|
||||
helper.assertTrue(refused, "External mutation must block startup before any absent chunk is staged");
|
||||
helper.assertTrue(java.util.Arrays.equals(before, MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(region))),
|
||||
"A rejected preflight must preserve the original region file byte for byte");
|
||||
helper.assertTrue(storage.read(pos).get(30, TimeUnit.SECONDS).orElseThrow().equals(mutated), "Foreign NBT must be preserved");
|
||||
storage.write(pos, original).get(30, TimeUnit.SECONDS); storage.synchronize(true).get(30, TimeUnit.SECONDS);
|
||||
// This deliberately simulates startup inside a running test; restore the runtime's deferred state afterward.
|
||||
ExpansionVoidReuse.open(level, generator, List.of(session.island("sanctuary")));
|
||||
for (var island : session.islands()) if (!island.reuseToken().isEmpty()) ExpansionVoidReuse.deferred(level, island);
|
||||
}
|
||||
private static List<ChunkPos> positions(ExpansionIsland island) {
|
||||
var result = new ArrayList<ChunkPos>(); int radius = island.terrainRadius() + ExpansionIsland.DECORATION_MARGIN;
|
||||
for (int z = (island.centerZ() - radius) >> 4; z <= (island.centerZ() + radius) >> 4; z++)
|
||||
for (int x = (island.centerX() - radius) >> 4; x <= (island.centerX() + radius) >> 4; x++)
|
||||
if (island.ownsChunk(x, z)) result.add(new ChunkPos(x, z));
|
||||
return result;
|
||||
}
|
||||
private static Path path(ServerLevel level, String name) { return level.getServer().getWorldPath(LevelResource.ROOT).resolve(name); }
|
||||
private static void write(ServerLevel level, String name, Map<String, Object> report) throws IOException {
|
||||
Files.writeString(path(level, name), new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
}
|
||||
}
|
||||
-533
@@ -1,533 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionAdmission;
|
||||
import fr.koka.sanctuary.expansion.ExpansionBiomeSource;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionJournal;
|
||||
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Opt-in end-to-end exercise on a new laboratory world; never rewrites chunks or a personal world. */
|
||||
public final class ExpansionWorldGameTests {
|
||||
private static final boolean OPTIONAL_STRUCTURE_WITNESS = Boolean.getBoolean("sanctuary.test.optionalStructureWitness");
|
||||
|
||||
@GameTest(maxTicks = 6000)
|
||||
public void islandsBranchPersistAndProtectVisitedChunks(GameTestHelper helper) {
|
||||
helper.assertTrue(Boolean.getBoolean("sanctuary.test.expansion"), "Expansion GameTest must be explicitly enabled");
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof ExpansionChunkGenerator,
|
||||
"Expansion GameTest must load the new laboratory preset");
|
||||
if (Boolean.getBoolean("sanctuary.test.expansionReload")) {
|
||||
verifyProcessReload(helper);
|
||||
return;
|
||||
}
|
||||
new Scenario(helper).advance();
|
||||
}
|
||||
|
||||
private static void verifyProcessReload(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var generator = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var session = ExpansionRuntime.session(level);
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("optional_native_structure_witness", OPTIONAL_STRUCTURE_WITNESS);
|
||||
report.put("scope", "A second Java process opens the same saved development chunks and expansion journal, and verifies a "
|
||||
+ "gold block written and flushed before the first shutdown. GameTestServer reconstructs its world settings from the preset; "
|
||||
+ "the prior generator codec is decoded separately here and its base columns are compared. This is not a normal-client "
|
||||
+ "world-selection test or a multiplayer/teleportation test.");
|
||||
try {
|
||||
JsonObject previous = JsonParser.parseString(Files.readString(diagnostic(level, false))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean(), "Reload requires a successful first integration run");
|
||||
helper.assertTrue(previous.has("restart_snapshot"), "The first process must save a restart witness before shutting down");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(),
|
||||
"The reload proof must run in another Java process");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed(), "Reload must keep the original world seed");
|
||||
JsonObject saved = previous.getAsJsonObject("restart_snapshot");
|
||||
List<ExpansionIsland> blockers = ExpansionIsland.CODEC.listOf().parse(JsonOps.INSTANCE, saved.get("blocked_sites")).getOrThrow();
|
||||
int savedRefusals = 0;
|
||||
for (var site : blockers) {
|
||||
helper.assertTrue(level.getChunkSource().chunkMap.getUpdatingChunkIfPresent(new ChunkPos(site.centerX() >> 4, site.centerZ() >> 4).pack()) == null,
|
||||
"Reload collision proof must start with an unloaded saved witness");
|
||||
try { ExpansionAdmission.verify(level, site); }
|
||||
catch (ExpansionAdmission.OccupiedException expected) {
|
||||
helper.assertTrue(expected.getMessage().contains("saved chunk"), "The reload proof must detect disk occupancy, not just a loaded holder");
|
||||
savedRefusals++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(savedRefusals == 8, "All eight first sites must remain unavailable on disk after restart");
|
||||
report.put("saved_only_collision_refusals", savedRefusals);
|
||||
helper.assertTrue(saved.get("world_directory").getAsString().equals(worldDirectory(level)),
|
||||
"Reload must use exactly the same development save directory");
|
||||
helper.assertTrue(session.islands().size() == 3 && session.islands().stream().allMatch(i -> i.status().equals("ready")),
|
||||
"The loaded journal must contain exactly the ready root, alpha and beta");
|
||||
helper.assertTrue(session.island("alpha").parent().equals("sanctuary") && session.island("beta").parent().equals("alpha"),
|
||||
"Reload must preserve the expansion branch");
|
||||
var encodedIslands = ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, session.islands()).getOrThrow();
|
||||
helper.assertTrue(encodedIslands.equals(saved.get("islands")), "Every persisted region field must survive the process restart");
|
||||
helper.assertTrue(sha256(Files.readString(journalFile(level))).equals(saved.get("journal_sha256").getAsString()),
|
||||
"Loading a ready journal must preserve its exact saved contents");
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var liveCodec = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
helper.assertTrue(liveCodec.equals(saved.get("generator_codec")), "Generator settings and biome holder identities must remain stable");
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, saved.get("generator_codec")).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof ExpansionChunkGenerator, "The saved generator codec must decode to the laboratory generator");
|
||||
var restored = (ExpansionChunkGenerator) decoded;
|
||||
restored.initialize(level, session.islands());
|
||||
|
||||
BlockPos witness = position(saved.getAsJsonArray("gold_witness"));
|
||||
full(helper, level, witness.getX() >> 4, witness.getZ() >> 4);
|
||||
helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK),
|
||||
"The first process's saved gold block must exist before the reload test writes anything");
|
||||
List<Map<String, Object>> samples = new ArrayList<>();
|
||||
for (var element : saved.getAsJsonArray("samples")) {
|
||||
JsonObject original = element.getAsJsonObject();
|
||||
var island = session.island(original.get("id").getAsString());
|
||||
BlockPos point = position(original.getAsJsonArray("point"));
|
||||
Map<String, Object> current = sample(helper, generator, island, point);
|
||||
helper.assertTrue(new GsonBuilder().create().toJsonTree(current).equals(original),
|
||||
"Stored blocks, stored biomes and generated base column must survive restart: " + island.id());
|
||||
helper.assertTrue(new GsonBuilder().create().toJsonTree(sample(helper, restored, island, point)).equals(original),
|
||||
"Decoding the saved generator codec must reproduce its base column and climate: " + island.id());
|
||||
samples.add(current);
|
||||
}
|
||||
boolean hasNativeStructure = previous.has("native_structure") && !previous.get("native_structure").isJsonNull();
|
||||
if (hasNativeStructure) {
|
||||
helper.assertTrue(!previous.has("native_structure_observed") || previous.get("native_structure_observed").getAsBoolean(),
|
||||
"Stored native structure evidence must not contradict the observation result");
|
||||
JsonObject nativeStructure = previous.getAsJsonObject("native_structure");
|
||||
BlockPos containerPos = position(nativeStructure.getAsJsonArray("container"));
|
||||
full(helper, level, containerPos.getX() >> 4, containerPos.getZ() >> 4);
|
||||
helper.assertTrue(level.getBlockState(containerPos).toString().equals(nativeStructure.get("placed_block").getAsString()),
|
||||
"The native structure's placed block state must survive restart");
|
||||
helper.assertTrue(level.getBlockEntity(containerPos) instanceof RandomizableContainerBlockEntity,
|
||||
"The native structure's placed container must be loaded from the saved chunk");
|
||||
var container = (RandomizableContainerBlockEntity) level.getBlockEntity(containerPos);
|
||||
helper.assertTrue(container.getLootTable() != null && container.getLootTable().identifier().toString()
|
||||
.equals(nativeStructure.get("loot_table").getAsString()), "The native structure loot table must survive restart unconsumed");
|
||||
report.put("native_container", coordinates(containerPos));
|
||||
report.put("native_structure_observed", true);
|
||||
report.put("native_container_verified", true);
|
||||
report.put("native_structure_status", "placed_block_and_unconsumed_loot_verified_after_restart");
|
||||
} else {
|
||||
helper.assertTrue(OPTIONAL_STRUCTURE_WITNESS
|
||||
&& previous.has("optional_native_structure_witness") && previous.get("optional_native_structure_witness").getAsBoolean()
|
||||
&& previous.has("native_structure_observed") && !previous.get("native_structure_observed").getAsBoolean()
|
||||
&& previous.has("native_structure_search_complete") && previous.get("native_structure_search_complete").getAsBoolean(),
|
||||
"Missing native structure evidence is allowed only after an explicitly optional, completed first-process search");
|
||||
report.put("native_structure_observed", false);
|
||||
report.put("native_container_verified", false);
|
||||
report.put("native_structure_status", "not_observed_after_complete_first_process_scan; no_native_structure_reload_proof");
|
||||
report.put("native_structure_limitation", "No natural structure with placed vanilla loot was observed in the first process. "
|
||||
+ "This reload verifies chunks, generator settings, the journal and expansion behavior, but supplies no positive structure proof.");
|
||||
}
|
||||
report.put("previous_process_id", previous.get("process_id").getAsLong());
|
||||
report.put("world_directory", worldDirectory(level));
|
||||
report.put("gold_witness", coordinates(witness));
|
||||
report.put("samples", samples);
|
||||
report.put("regions", session.islands().size());
|
||||
report.put("saved_generator_codec_decoded", true);
|
||||
var firstCandidate = session.candidate("after_reload", "sanctuary", "south", "boreal", 64, "natural");
|
||||
var opened = session.create("after_reload", "sanctuary", "south", "boreal", 64, "natural");
|
||||
helper.assertTrue(opened.centerZ() > firstCandidate.centerZ() && opened.centerX() == 0,
|
||||
"Creation must skip the saved occupied first site and preserve the requested southern ray");
|
||||
report.put("after_reload_expansion", List.of(opened.centerX(), opened.centerZ()));
|
||||
awaitReloadExpansion(helper, session, blockers, witness, report);
|
||||
} catch (Exception exception) {
|
||||
report.put("passed", false);
|
||||
report.put("failure", exception.toString());
|
||||
try { writeDiagnostic(level, true, report); } catch (IOException failure) { exception.addSuppressed(failure); }
|
||||
SanctuaryMod.LOGGER.error("Expansion process reload scenario failed", exception);
|
||||
helper.fail(exception.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitReloadExpansion(GameTestHelper helper, ExpansionRuntime.Session session,
|
||||
List<ExpansionIsland> blockers, BlockPos gold, Map<String, Object> report) {
|
||||
try {
|
||||
helper.assertTrue(helper.getTick() < 5000, "Reload expansion preparation deadline: " + session.progress());
|
||||
if (!session.island("after_reload").status().equals("ready")) {
|
||||
helper.runAtTickTime(helper.getTick() + 10, () -> awaitReloadExpansion(helper, session, blockers, gold, report));
|
||||
return;
|
||||
}
|
||||
verifyUntouchedVoid(helper, blockers);
|
||||
helper.assertTrue(helper.getLevel().getBlockState(gold).is(Blocks.GOLD_BLOCK), "Opening farther away must preserve the saved player witness");
|
||||
var landing = session.landing("after_reload");
|
||||
helper.assertTrue(ExpansionRuntime.safeLanding(helper.getLevel(), landing), "The new region after restart must reach usable terrain");
|
||||
report.put("after_reload_landing", coordinates(landing));
|
||||
report.put("final_regions", session.islands().size());
|
||||
report.put("passed", true);
|
||||
writeDiagnostic(helper.getLevel(), true, report);
|
||||
helper.succeed();
|
||||
} catch (Exception exception) {
|
||||
report.put("passed", false);
|
||||
report.put("failure", exception.toString());
|
||||
try { writeDiagnostic(helper.getLevel(), true, report); } catch (IOException failure) { exception.addSuppressed(failure); }
|
||||
helper.fail(exception.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyUntouchedVoid(GameTestHelper helper, List<ExpansionIsland> blockers) {
|
||||
var generator = (ExpansionChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
for (var site : blockers) {
|
||||
var chunk = full(helper, helper.getLevel(), site.centerX() >> 4, site.centerZ() >> 4);
|
||||
helper.assertTrue(generator.islandAt(site.centerX(), site.centerZ()) == null
|
||||
&& Arrays.stream(chunk.getSections()).allMatch(section -> section.hasOnlyAir()),
|
||||
"An occupied saved void chunk must remain unchanged after farther placement: " + site.direction());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
private final GameTestHelper helper;
|
||||
private final ServerLevel level;
|
||||
private final ExpansionChunkGenerator generator;
|
||||
private final ExpansionRuntime.Session session;
|
||||
private final Map<String, Object> report = new LinkedHashMap<>();
|
||||
private final List<Map<String, Object>> observedIslands = new ArrayList<>();
|
||||
private final Map<String, BlockPos> samplePoints = new LinkedHashMap<>();
|
||||
private final List<ChunkPos> structureSearch = new ArrayList<>();
|
||||
private final long started = System.nanoTime();
|
||||
private ExpansionIsland alpha;
|
||||
private ExpansionIsland beta;
|
||||
private BlockPos marker;
|
||||
private BlockState previousMarker;
|
||||
private int phase;
|
||||
private int structureIndex;
|
||||
private int validStarts;
|
||||
private final List<ExpansionIsland> blockedSites = new ArrayList<>();
|
||||
|
||||
Scenario(GameTestHelper helper) {
|
||||
this.helper = helper;
|
||||
this.level = helper.getLevel();
|
||||
this.generator = (ExpansionChunkGenerator) level.getChunkSource().getGenerator();
|
||||
this.session = ExpansionRuntime.session(level);
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("generator_version", generator.rootGeneration());
|
||||
report.put("initial_diameter", generator.initialDiameter());
|
||||
report.put("islands", observedIslands);
|
||||
report.put("optional_native_structure_witness", OPTIONAL_STRUCTURE_WITNESS);
|
||||
report.put("native_structure_observed", false);
|
||||
report.put("native_structure_search_complete", false);
|
||||
report.put("native_structure_status", "not_searched");
|
||||
report.put("scope", "Fresh versioned Sanctuary development world; real FULL chunks, a natural native-structure search, asynchronous activation, "
|
||||
+ "branching, journal re-read and loaded void admission refusal. Journal re-read is not a process restart. "
|
||||
+ "The parent block witness represents a player edit; no player teleport, multiplayer or client rendering is simulated. "
|
||||
+ (OPTIONAL_STRUCTURE_WITNESS ? "The native structure witness is explicitly optional; an absent witness is reported as missing evidence, never as a successful structures test."
|
||||
: "A naturally accepted structure with placed vanilla loot is required."));
|
||||
}
|
||||
|
||||
void advance() {
|
||||
try {
|
||||
helper.assertTrue(helper.getTick() < 5800, "Expansion scenario deadline: " + session.progress());
|
||||
switch (phase) {
|
||||
case 0 -> start();
|
||||
case 1 -> {
|
||||
if (!session.island("alpha").status().equals("ready")) break;
|
||||
helper.assertTrue(session.island("alpha").equals(alpha.ready()), "Activation must only advance the immutable island state");
|
||||
observeIsland("alpha", Set.of("jungle", "sparse_jungle", "bamboo_jungle", "mangrove_swamp"));
|
||||
verifyMarker();
|
||||
beta = session.create("beta", "alpha", "east", "arid", 64, "natural", 2048);
|
||||
helper.assertTrue(beta.parent().equals("alpha") && beta.centerX() > alpha.centerX()
|
||||
&& beta.centerZ() == alpha.centerZ(), "A child must branch east from alpha, not from world origin");
|
||||
helper.assertTrue(beta.centerX() - alpha.centerX() >= 2048, "The requested minimum distance must be honoured from the chosen parent");
|
||||
verifyJournal();
|
||||
phase = 2;
|
||||
}
|
||||
case 2 -> {
|
||||
if (!session.island("beta").status().equals("ready")) break;
|
||||
helper.assertTrue(session.island("beta").equals(beta.ready()), "Branch activation must preserve its planned identity");
|
||||
observeIsland("beta", Set.of("desert", "badlands", "eroded_badlands", "wooded_badlands"));
|
||||
verifyMarker();
|
||||
verifyJournal();
|
||||
verifyUntouchedVoid(helper, blockedSites);
|
||||
prepareStructureSearch();
|
||||
phase = 3;
|
||||
}
|
||||
case 3 -> {
|
||||
if (observeNativeStructure()) {
|
||||
verifyMarker();
|
||||
verifyJournal();
|
||||
level.setBlock(marker, previousMarker, 3);
|
||||
prepareRestartWitness();
|
||||
report.put("islands", observedIslands);
|
||||
report.put("native_structure_starts_seen", validStarts);
|
||||
report.put("structure_start_chunks_inspected", structureIndex);
|
||||
report.put("ticks", helper.getTick());
|
||||
report.put("elapsed_ms", (System.nanoTime() - started) / 1_000_000.0);
|
||||
report.put("passed", true);
|
||||
writeReport();
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unknown expansion test phase");
|
||||
}
|
||||
if (helper.getTick() % 200 == 0) SanctuaryMod.LOGGER.info("Expansion scenario phase {}: {}", phase, session.progress());
|
||||
helper.runAtTickTime(helper.getTick() + 10, this::advance);
|
||||
} catch (Exception exception) {
|
||||
report.put("passed", false);
|
||||
report.put("failure", exception.toString());
|
||||
report.put("phase", phase);
|
||||
report.put("runtime_progress", session.progress());
|
||||
try { writeReport(); } catch (IOException failure) { exception.addSuppressed(failure); }
|
||||
SanctuaryMod.LOGGER.error("Expansion integration scenario failed", exception);
|
||||
helper.fail(exception.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void start() throws IOException {
|
||||
helper.assertTrue(session.islands().size() == 1, "The test must start from a new expansion journal");
|
||||
helper.assertTrue(level.structureManager().shouldGenerateStructures(), "The laboratory GameTest must enable actual vanilla structures in WorldOptions");
|
||||
ExpansionCommandChecks.verify(helper);
|
||||
report.put("registered_command_permission_checks", true);
|
||||
helper.assertTrue(generator.source().possibleBiomes().size() >= ExpansionBiomeSource.REQUIRED_BIOMES.size(),
|
||||
"Future biome holders must be declared before the first expansion");
|
||||
BlockPos landing = session.landing("sanctuary");
|
||||
helper.assertTrue(ExpansionRuntime.safeLanding(level, landing), "The initial laboratory island must provide a real safe landing");
|
||||
full(landing.getX() >> 4, landing.getZ() >> 4);
|
||||
marker = landing.above(6);
|
||||
previousMarker = level.getBlockState(marker);
|
||||
helper.assertTrue(level.setBlock(marker, Blocks.DIAMOND_BLOCK.defaultBlockState(), 3), "Place the parent player-edit witness");
|
||||
report.put("parent_witness", coordinates(marker));
|
||||
report.put("root_landing", coordinates(landing));
|
||||
samplePoints.put("sanctuary", landing);
|
||||
|
||||
for (String direction : ExpansionIsland.DIRECTIONS) {
|
||||
var site = session.candidate("blocked_" + direction, "sanctuary", direction, "boreal", 64, "natural");
|
||||
var chunk = full(site.centerX() >> 4, site.centerZ() >> 4);
|
||||
helper.assertTrue(Arrays.stream(chunk.getSections()).allMatch(section -> section.hasOnlyAir()), "The first site must initially contain only real generated void");
|
||||
blockedSites.add(site);
|
||||
}
|
||||
int occupied = 0;
|
||||
for (var site : blockedSites) {
|
||||
try { ExpansionAdmission.verify(level, site); }
|
||||
catch (ExpansionAdmission.OccupiedException expected) { occupied++; }
|
||||
}
|
||||
helper.assertTrue(occupied == 8, "Reproduce the user failure with all eight first directions occupied");
|
||||
report.put("initial_occupied_directions", occupied);
|
||||
var nearestNorth = session.candidate("alpha", "sanctuary", "north", "tropical", 64, "natural");
|
||||
|
||||
alpha = session.create("alpha", "sanctuary", "north", "tropical", 64, "natural");
|
||||
helper.assertTrue(alpha.status().equals("reserved") && alpha.centerZ() < 0 && alpha.centerX() == 0,
|
||||
"North placement and the explicitly chosen tropical climate must remain independent");
|
||||
helper.assertTrue(alpha.centerZ() < nearestNorth.centerZ(), "Automatic placement must skip occupied chunks and continue farther north");
|
||||
helper.assertTrue(alpha.equals(session.create("alpha", "sanctuary", "north", "tropical", 64, "natural"))
|
||||
&& session.islands().size() == 2, "Repeating a matching request must not create another region or change its identity");
|
||||
boolean conflict = false;
|
||||
try { session.create("alpha", "sanctuary", "north", "arid", 64, "natural"); }
|
||||
catch (IllegalArgumentException expected) { conflict = expected.getMessage().equals("id_conflict"); }
|
||||
helper.assertTrue(conflict, "A conflicting duplicate island id must be refused");
|
||||
verifyJournal();
|
||||
phase = 1;
|
||||
}
|
||||
|
||||
private void observeIsland(String id, Set<String> expectedSurfaceBiomes) {
|
||||
ExpansionIsland island = session.island(id);
|
||||
BlockPos landing = session.landing(id);
|
||||
helper.assertTrue(island.contains(landing.getX(), landing.getZ()) && ExpansionRuntime.safeLanding(level, landing),
|
||||
"READY must offer actual safe terrain inside " + id);
|
||||
LevelChunk chunk = full(landing.getX() >> 4, landing.getZ() >> 4);
|
||||
helper.assertTrue(Arrays.stream(chunk.getSections()).anyMatch(section -> !section.hasOnlyAir()), "READY cannot be only a registry entry: " + id);
|
||||
// The 240 sample is the stored upper climate in a FULL chunk, independent of which safe ledge was found.
|
||||
String climateBiome = chunk.getNoiseBiome(landing.getX() >> 2, 60, landing.getZ() >> 2).unwrapKey().orElseThrow().identifier().toString();
|
||||
helper.assertTrue(expectedSurfaceBiomes.contains(climateBiome.substring("minecraft:".length())),
|
||||
"The FULL chunk must contain its requested upper climate: " + id + " / " + climateBiome);
|
||||
String landingBiome = level.getBiome(landing).unwrapKey().orElseThrow().identifier().toString();
|
||||
samplePoints.put(id, landing);
|
||||
observedIslands.add(Map.of("id", id, "parent", island.parent(), "climate", island.climate(),
|
||||
"centre", List.of(island.centerX(), island.centerZ()), "diameter", island.diameter(),
|
||||
"landing", coordinates(landing), "landing_biome", landingBiome, "upper_biome", climateBiome));
|
||||
}
|
||||
|
||||
private void verifyMarker() {
|
||||
helper.assertTrue(level.getBlockState(marker).is(Blocks.DIAMOND_BLOCK), "An expansion must preserve the parent player-edit witness");
|
||||
}
|
||||
|
||||
private void verifyJournal() throws IOException {
|
||||
Path directory = level.getServer().getWorldPath(LevelResource.ROOT).resolve("data/" + ((ExpansionChunkGenerator) level.getChunkSource().getGenerator()).journalDirectory());
|
||||
var reloaded = new ExpansionJournal(directory, level.getSeed(), generator.initialDiameter(), generator.rootGeneration());
|
||||
helper.assertTrue(reloaded.islands().equals(session.islands()), "Re-reading the saved journal must preserve all region identities and states");
|
||||
report.put("journal_reloaded_regions", reloaded.islands().size());
|
||||
}
|
||||
|
||||
private void prepareRestartWitness() throws IOException {
|
||||
BlockPos gold = samplePoints.get("beta").above(6);
|
||||
while (!level.getBlockState(gold).isAir() && gold.getY() < level.getMaxY() - 1) gold = gold.above();
|
||||
helper.assertTrue(gold.getY() < level.getMaxY() - 1 && level.getBlockState(gold).isAir(), "The restart witness needs empty space above beta");
|
||||
helper.assertTrue(level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3), "Write the restart witness in the first process");
|
||||
var snapshot = new LinkedHashMap<String, Object>();
|
||||
snapshot.put("world_directory", worldDirectory(level));
|
||||
snapshot.put("gold_witness", coordinates(gold));
|
||||
snapshot.put("journal_sha256", sha256(Files.readString(journalFile(level))));
|
||||
snapshot.put("islands", ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, session.islands()).getOrThrow());
|
||||
snapshot.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
|
||||
snapshot.put("blocked_sites", ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, blockedSites).getOrThrow());
|
||||
List<Map<String, Object>> samples = new ArrayList<>();
|
||||
for (var island : session.islands()) samples.add(sample(helper, generator, island, samplePoints.get(island.id())));
|
||||
snapshot.put("samples", samples);
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush the first process's world and marker before writing restart evidence");
|
||||
snapshot.put("world_flushed_before_shutdown", true);
|
||||
report.put("restart_snapshot", snapshot);
|
||||
}
|
||||
|
||||
private void prepareStructureSearch() {
|
||||
ExpansionIsland root = session.island("sanctuary");
|
||||
int limit = (root.terrainRadius() + 15) / 16;
|
||||
// STARTS only, centre outwards. Only an accepted structure's region is later requested at FULL.
|
||||
for (int ring = 0; ring <= limit; ring++) for (int x = -ring; x <= ring; x++) for (int z = -ring; z <= ring; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) == ring && root.contains(x * 16 + 8, z * 16 + 8)) structureSearch.add(new ChunkPos(x, z));
|
||||
}
|
||||
report.put("structure_search_candidate_chunks", structureSearch.size());
|
||||
report.put("native_structure_status", "search_in_progress");
|
||||
}
|
||||
|
||||
private boolean observeNativeStructure() {
|
||||
int end = Math.min(structureSearch.size(), structureIndex + 16);
|
||||
while (structureIndex < end) {
|
||||
ChunkPos position = structureSearch.get(structureIndex++);
|
||||
var chunk = level.getChunkSource().getChunk(position.x(), position.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
helper.assertTrue(chunk != null, "A structure-start request must complete");
|
||||
for (StructureStart start : chunk.getAllStarts().values()) {
|
||||
if (!start.isValid()) continue;
|
||||
validStarts++;
|
||||
String name = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(start.getStructure()).toString();
|
||||
if (!name.startsWith("minecraft:")) continue;
|
||||
Map<String, Object> evidence = materializeAndFindLoot(start, name);
|
||||
if (!evidence.isEmpty()) {
|
||||
report.put("native_structure", evidence);
|
||||
report.put("native_structure_observed", true);
|
||||
report.put("native_structure_status", "natural_start_with_placed_vanilla_loot_observed");
|
||||
report.put("native_structure_search_complete", structureIndex == structureSearch.size());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
report.put("native_structure_starts_seen", validStarts);
|
||||
report.put("structure_start_chunks_inspected", structureIndex);
|
||||
if (structureIndex == structureSearch.size()) {
|
||||
report.put("native_structure_search_complete", true);
|
||||
report.put("native_structure_status", "not_observed_after_complete_scan");
|
||||
report.put("native_structure_limitation", "No naturally accepted native structure with placed vanilla loot was observed after "
|
||||
+ structureIndex + " root-island start chunks and " + validStarts + " valid starts. "
|
||||
+ "This is missing engine evidence for structures, not a positive generation or loot result.");
|
||||
if (OPTIONAL_STRUCTURE_WITNESS) return true;
|
||||
}
|
||||
helper.assertTrue(structureIndex < structureSearch.size(), "No naturally accepted native structure with placed vanilla loot was observed; "
|
||||
+ validStarts + " valid starts. This is a missing engine proof, not a successful structures test.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, Object> materializeAndFindLoot(StructureStart start, String name) {
|
||||
var bounds = start.getBoundingBox();
|
||||
List<LevelChunk> chunks = new ArrayList<>();
|
||||
for (int x = bounds.minX() >> 4; x <= bounds.maxX() >> 4; x++) {
|
||||
for (int z = bounds.minZ() >> 4; z <= bounds.maxZ() >> 4; z++) chunks.add(full(x, z));
|
||||
}
|
||||
for (LevelChunk chunk : chunks) {
|
||||
for (var entry : chunk.getBlockEntities().entrySet()) {
|
||||
if (!(entry.getValue() instanceof RandomizableContainerBlockEntity container) || container.getLootTable() == null) continue;
|
||||
BlockPos pos = entry.getKey();
|
||||
if (start.getPieces().stream().noneMatch(piece -> piece.getBoundingBox().isInside(pos))) continue;
|
||||
String loot = container.getLootTable().identifier().toString();
|
||||
if (!loot.startsWith("minecraft:")) continue;
|
||||
return Map.of("structure", name, "loot_table", loot, "container", coordinates(pos),
|
||||
"placed_block", level.getBlockState(pos).toString(), "full_chunks", chunks.size(),
|
||||
"mode", "natural candidate accepted by production ExpansionStructures, native decoration");
|
||||
}
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private LevelChunk full(int x, int z) {
|
||||
var chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Expected real FULL chunk at " + new ChunkPos(x, z));
|
||||
return (LevelChunk) chunk;
|
||||
}
|
||||
|
||||
private void writeReport() throws IOException {
|
||||
writeDiagnostic(level, false, report);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> sample(GameTestHelper helper, ExpansionChunkGenerator generator, ExpansionIsland island, BlockPos point) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
LevelChunk chunk = full(helper, level, point.getX() >> 4, point.getZ() >> 4);
|
||||
var random = generator.randomState(island);
|
||||
var base = generator.getBaseColumn(point.getX(), point.getZ(), level, random);
|
||||
StringBuilder storedBlocks = new StringBuilder(), baseBlocks = new StringBuilder(), biomes = new StringBuilder(), resolvedBiomes = new StringBuilder();
|
||||
var resolver = generator.source().createUncachedResolver(random);
|
||||
for (int y = level.getMinY(); y <= level.getMaxY(); y++) {
|
||||
storedBlocks.append(chunk.getBlockState(new BlockPos(point.getX(), y, point.getZ()))).append('\n');
|
||||
baseBlocks.append(base.getBlock(y)).append('\n');
|
||||
if ((y & 3) == 0) {
|
||||
biomes.append(chunk.getNoiseBiome(point.getX() >> 2, y >> 2, point.getZ() >> 2).getRegisteredName()).append('\n');
|
||||
resolvedBiomes.append(resolver.getNoiseBiome(point.getX() >> 2, y >> 2, point.getZ() >> 2).getRegisteredName()).append('\n');
|
||||
}
|
||||
}
|
||||
return Map.of("id", island.id(), "point", coordinates(point), "stored_column_sha256", sha256(storedBlocks.toString()),
|
||||
"base_column_sha256", sha256(baseBlocks.toString()), "stored_biomes_sha256", sha256(biomes.toString()),
|
||||
"resolved_biomes_sha256", sha256(resolvedBiomes.toString()), "base_height",
|
||||
generator.getBaseHeight(point.getX(), point.getZ(), Heightmap.Types.WORLD_SURFACE_WG, level, random));
|
||||
}
|
||||
|
||||
private static LevelChunk full(GameTestHelper helper, ServerLevel level, int x, int z) {
|
||||
var chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Expected saved FULL chunk at " + new ChunkPos(x, z));
|
||||
return (LevelChunk) chunk;
|
||||
}
|
||||
|
||||
private static String sha256(String text) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(text.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); }
|
||||
}
|
||||
|
||||
private static Path journalFile(ServerLevel level) {
|
||||
return level.getServer().getWorldPath(LevelResource.ROOT).resolve("data/" + ((ExpansionChunkGenerator) level.getChunkSource().getGenerator()).journalDirectory()).resolve(ExpansionJournal.FILE_NAME);
|
||||
}
|
||||
private static String worldDirectory(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString(); }
|
||||
private static Path diagnostic(ServerLevel level, boolean reload) {
|
||||
return Path.of("diagnostics", (Boolean.getBoolean("sanctuary.test.unified") ? "expansion-alpha13-" : "expansion-alpha12-1-") + (reload ? "reload-" : "") + "seed-" + level.getSeed() + ".json");
|
||||
}
|
||||
private static void writeDiagnostic(ServerLevel level, boolean reload, Map<String, Object> report) throws IOException {
|
||||
Path output = diagnostic(level, reload);
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
}
|
||||
private static BlockPos position(JsonArray coordinates) { return new BlockPos(coordinates.get(0).getAsInt(), coordinates.get(1).getAsInt(), coordinates.get(2).getAsInt()); }
|
||||
private static List<Integer> coordinates(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
}
|
||||
-452
@@ -1,452 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
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;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation19 starter island. */
|
||||
public final class Heritage19WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 19, "Use a fresh generation19 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage19 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage19 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 19
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation19 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece19.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece19, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece19.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece19.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece19) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece19(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 20);
|
||||
boolean rejected = false; try { new HeritagePiece19(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage19WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
|
||||
int requiredSpan = generator.initialDiameter() / 4;
|
||||
report.put("reference_minimum_route_span", requiredSpan);
|
||||
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
|
||||
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
|
||||
}
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece19.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece19 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer19.render(piece.site(), writer); else HeritageRenderer19.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece19(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece19.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer19.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage19-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage19-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
-556
@@ -1,556 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece20;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer20;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
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;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation20 starter island. */
|
||||
public final class Heritage20WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 20, "Use a fresh generation20 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
final List<StructureStart> ruinsStarts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage20 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage20 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 20
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation20 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece20.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece20, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece20.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
var ruins = generator.ruinsPlan17();
|
||||
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
|
||||
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
|
||||
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
|
||||
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var ruinsOrigins = new HashSet<ChunkPos>();
|
||||
for (var network : ruins.networks()) {
|
||||
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
|
||||
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
|
||||
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
|
||||
ruinsStarts.add(nativeStart);
|
||||
for (var piece : nativeStart.getPieces()) {
|
||||
helper.assertTrue(piece instanceof RuinsPiece20, "Generation20 loaded a historical forgotten piece");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece20.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece20.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece20) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece20(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 21);
|
||||
boolean rejected = false; try { new HeritagePiece20(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("connected_blocks", connections);
|
||||
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
|
||||
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
|
||||
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
|
||||
verifyRuins();
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage20WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724) {
|
||||
int requiredSpan = generator.initialDiameter() / 4;
|
||||
report.put("reference_minimum_route_span", requiredSpan);
|
||||
helper.assertTrue(greatestSpan >= requiredSpan, "Seed0 Medium must demonstrate an island-scale route spanning at least "
|
||||
+ requiredSpan + " blocks on X or Z; got " + greatestSpan);
|
||||
}
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece20.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
void verifyRuins() {
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : ruinsStarts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
|
||||
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
|
||||
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
|
||||
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
|
||||
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
|
||||
}
|
||||
for (var raw : start.getPieces()) {
|
||||
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
|
||||
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
|
||||
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
|
||||
var expected = rendered(room); blockCount += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
|
||||
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
|
||||
}
|
||||
}
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
|
||||
}
|
||||
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
|
||||
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
|
||||
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece20 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer20.render(piece.site(), writer); else HeritageRenderer20.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Rendered rendered(RuinsPlan17.Room room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
});
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
|
||||
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
|
||||
var hatches = new HashMap<BlockPos, BlockState>();
|
||||
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
|
||||
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
|
||||
hatches.forEach((at, state) -> {
|
||||
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
|
||||
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
});
|
||||
try {
|
||||
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
|
||||
int targets = 0;
|
||||
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
|
||||
var point = at(room, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
|
||||
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
|
||||
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
|
||||
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece20(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece20.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer20.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage20-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage20-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
-554
@@ -1,554 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePiece21;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritagePlan19;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageRenderer21;
|
||||
import fr.koka.sanctuary.worldgen.heritage.HeritageStructures19;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer20;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
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;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native creation/reload proof confined to a disposable generation21 starter island. */
|
||||
public final class Heritage21WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void heritagePersistsWithRealDestinations(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The heritage proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 21, "Use a fresh generation21 test world; do not convert an old save");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final HeritagePlan19 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
final List<StructureStart> ruinsStarts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.heritagePlan19();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native local starts, saved sites and routes, player-sized entrances, supports and unopened chests on the starter island. "
|
||||
+ "All14 tool tables are sampled separately without consuming a saved chest. Door states are restored after traversal. "
|
||||
+ "No personal save or expansion is opened; one disposable outer-wall edit is checked in a second JVM. "
|
||||
+ "Missing rails are intentional; this proof checks walking and saved rail shapes, not an uninterrupted minecart journey.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) { report.put("stage", "prepare"); prepare(); prepared = true; if (plan.isEmpty()) { finishDisabled(); return; } }
|
||||
if (nextChunk < chunks.size()) {
|
||||
report.put("stage", "load_full_chunks"); var pos = chunks.get(nextChunk++);
|
||||
helper.assertTrue(level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Heritage footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 20 == 0) SanctuaryMod.LOGGER.info("Heritage21 loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
report.put("stage", "verify_native_geometry"); verifyPlaced(); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold), "Gold witness must replace a solid wall away from circulation");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3); report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native starts, unopened loot and the wall edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the player's wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Heritage21 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 21
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "Generation21 codec lost its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan));
|
||||
report.put("planned_sites", plan.sites().stream().map(site -> Map.of("id", site.id(), "kind", site.kind().name(),
|
||||
"entry", coords(HeritagePiece21.locator(site)), "floor_y", site.floorY(), "foundation_depth", site.foundationDepth(), "footings", site.supports().size())).toList());
|
||||
report.put("planned_links", plan.links().stream().map(link -> Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(), "path", pathCoords(link))).toList());
|
||||
report.put("planning_samples", plan.samples()); report.put("planning_columns", plan.columns()); report.put("planning_diagnostics", plan.diagnostics());
|
||||
helper.assertTrue(plan.samples() <= HeritagePlan19.MAX_SAMPLES && plan.columns() <= HeritagePlan19.MAX_COLUMNS, "Heritage planning exceeded its budget");
|
||||
verifyLootTables(helper, report);
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload needs a successful flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must use a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload selected another world");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)), "Saved settings or planned identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
if (!generator.experimentalStructures() || !level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(plan.isEmpty() && plan.segments().isEmpty() && plan.links().isEmpty(), "Disabled generation published heritage geometry"); return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no heritage site");
|
||||
report.put("present_site_kinds", plan.sites().stream().map(s -> s.kind().name()).distinct().sorted().toList());
|
||||
report.put("missing_site_kinds", java.util.Arrays.stream(HeritagePlan19.Kind.values()).filter(k -> plan.sites().stream().noneMatch(s -> s.kind() == k)).map(Enum::name).toList());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var origins = new HashSet<ChunkPos>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
plan.sites().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
plan.segments().forEach(s -> origins.add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
for (var origin : origins.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) {
|
||||
var chunk = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted site/route has no native start at " + origin); starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof HeritagePiece21, "Heritage loaded a foreign piece type");
|
||||
var box = piece.getBoundingBox();
|
||||
helper.assertTrue(Math.abs((long) (box.minX() >> 4) - origin.x()) <= 7 && Math.abs((long) (box.maxX() >> 4) - origin.x()) <= 7
|
||||
&& Math.abs((long) (box.minZ() >> 4) - origin.z()) <= 7 && Math.abs((long) (box.maxZ() >> 4) - origin.z()) <= 7, "Native start references a distant route endpoint");
|
||||
box.intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
for (var site : plan.sites()) uniqueChunks.add(ChunkPos.containing(HeritagePiece21.locator(site)));
|
||||
for (var link : plan.links()) for (var point : link.path()) uniqueChunks.add(new ChunkPos(point.x() >> 4, point.z() >> 4));
|
||||
helper.assertTrue(uniqueChunks.size() <= 1024, "Bounded proof footprint exceeds1024 chunks");
|
||||
var ruins = generator.ruinsPlan17();
|
||||
report.put("ruins_plan_sha256", hash(ruins.networks().toString()));
|
||||
if (reload) helper.assertTrue(previous.get("ruins_plan_sha256").getAsString().equals(hash(ruins.networks().toString())), "Reload changed forgotten room planning");
|
||||
helper.assertTrue(!ruins.isEmpty(), "Reference20 must demonstrate native experimental room architecture");
|
||||
var ruinsType = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var ruinsOrigins = new HashSet<ChunkPos>();
|
||||
for (var network : ruins.networks()) {
|
||||
var origin = new ChunkPos(network.startChunkX(), network.startChunkZ()); if (!ruinsOrigins.add(origin)) continue;
|
||||
var nativeStart = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(ruinsType.value());
|
||||
helper.assertTrue(nativeStart != null && nativeStart.isValid(), "Missing native20 forgotten network: " + network.id());
|
||||
ruinsStarts.add(nativeStart);
|
||||
for (var piece : nativeStart.getPieces()) {
|
||||
helper.assertTrue(piece instanceof RuinsPiece20, "Generation21 loaded a historical forgotten piece");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
chunks.addAll(uniqueChunks.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var start = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled heritage emitted a native start");
|
||||
helper.assertTrue(generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false) == null, "Disabled heritage is locatable");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) { helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save disabled experiment setting"); report.put("world_flushed_before_shutdown", true); }
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(HeritageStructures19.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level); var summaries = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, chests = 0, pieces = 0, supports = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); String nativeHash = hash(saved.toString());
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "Native start failed an exact NBT roundtrip");
|
||||
var expectedPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
plan.sites().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSite(s)));
|
||||
plan.segments().stream().filter(s -> s.startChunkX() == origin.x() && s.startChunkZ() == origin.z()).forEach(s -> expectedPayloads.add(HeritagePiece21.encodeSegment(s)));
|
||||
var actualPayloads = new HashSet<net.minecraft.nbt.CompoundTag>();
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash), "Reload changed saved geometry or native reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var value : start.getPieces()) {
|
||||
var piece = (HeritagePiece21) value; pieces++;
|
||||
var tag = piece.createTag(context); var restored = new HeritagePiece21(context, tag);
|
||||
helper.assertTrue(restored.createTag(context).equals(tag), "Saved piece parameters changed on native load");
|
||||
actualPayloads.add(tag.getCompound("SanctuaryHeritage").orElseThrow());
|
||||
var corrupt = tag.copy(); corrupt.getCompound("SanctuaryHeritage").orElseThrow().putInt("Schema", 22);
|
||||
boolean rejected = false; try { new HeritagePiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "Unknown schema must fail closed");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true)
|
||||
.getReferencesForStructure(structure.value()).contains(origin.pack()), "FULL chunk lost its native structure reference"));
|
||||
var expected = rendered(piece); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue(); helper.assertTrue(piece.getBoundingBox().isInside(at), "Renderer escaped saved bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()), "Clipping or decoration changed heritage at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!wanted.is(Blocks.SPAWNER), "Heritage must not create spawners");
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), reload ? Set.of(gold) : Set.of()));
|
||||
if (piece.isSite()) {
|
||||
supports += verifySiteBearing(helper, generator, plan, piece.site());
|
||||
if (!reload && gold == null) {
|
||||
gold = expected.blocks().entrySet().stream().filter(e -> isOuterWall(piece.site(), e.getKey()) && e.getValue().isCollisionShapeFullBlock(level, e.getKey()))
|
||||
.map(Map.Entry::getKey).sorted(Comparator.<BlockPos>comparingInt(p -> p.getY()).thenComparingInt(p -> p.getX()).thenComparingInt(p -> p.getZ())).findFirst().orElse(null);
|
||||
}
|
||||
} else for (var cell : piece.segment().cells()) { supports++; assertRock(helper, new BlockPos(cell.x(), cell.minY() - 1, cell.z())); }
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred chest is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(entity.getLootTable() != null && entity.getLootTable().equals(chest.table()) && entity.getLootTableSeed() == chest.seed(), "Deferred loot table/seed was lost or consumed");
|
||||
if (piece.isSite()) helper.assertTrue(chest.table().equals(lootKey(piece.site().kind())), "Site uses another kind's tool table");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(actualPayloads.equals(expectedPayloads) && start.getPieces().size() == expectedPayloads.size(), "Native start lost or duplicated planned pieces");
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash, "pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("connected_blocks", connections);
|
||||
helper.assertTrue(connections.panes() > 0 && connections.stairs() > 0 && connections.nativeOracles() > 0,
|
||||
"Reference20 needs real panes/stairs validated by vanilla neighbor rules");
|
||||
if (level.getSeed() == 0 && generator.initialDiameter() == 724)
|
||||
helper.assertTrue(connections.crossChunkPaneLinks() > 0, "Reference20 must demonstrate a real pane join across a chunk boundary");
|
||||
verifyRuins();
|
||||
report.put("starts", summaries); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports); report.put("unopened_deferred_chests", chests);
|
||||
report.put("stage", "verify_site_routes"); var sites = new ArrayList<Map<String, Object>>(); for (var site : plan.sites()) sites.add(verifyAccess(helper, site)); report.put("native_sites", sites);
|
||||
report.put("stage", "verify_read_only_landings");
|
||||
report.put("read_only_landmarks", verifyReadOnlyLandings(helper, generator, plan));
|
||||
report.put("stage", "verify_island_routes"); report.put("native_links", verifyLinks(helper, plan));
|
||||
int greatestSpan = plan.links().stream().mapToInt(Heritage21WorldGameTests::routeSpan).max().orElse(0);
|
||||
report.put("greatest_single_route_span", greatestSpan);
|
||||
var reachedIds = new HashSet<String>(); plan.links().forEach(l -> { reachedIds.add(l.fromId()); reachedIds.add(l.toId()); });
|
||||
report.put("connected_destination_ids", reachedIds.stream().sorted().toList());
|
||||
report.put("isolated_destination_ids", plan.sites().stream().map(HeritagePlan19.Site::id).filter(id -> !reachedIds.contains(id)).toList());
|
||||
report.put("connected_city_arrivals", plan.landmarks().stream().filter(l -> l.kind() == HeritagePlan19.LandmarkKind.CITY && reachedIds.contains(l.id())).map(HeritagePlan19.Landmark::id).toList());
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(HeritageStructures19.KEY) && plan.sites().stream().anyMatch(s -> HeritagePiece21.locator(s).equals(located.getFirst())), "Locate must lead to a real exterior site entry");
|
||||
report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
void verifyRuins() {
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var summaries = new ArrayList<Map<String, Object>>(); int count = 0, blockCount = 0, lootCount = 0;
|
||||
var connections = NativeConnections20.Stats.empty();
|
||||
for (var start : ruinsStarts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin); var digest = hash(saved.toString());
|
||||
var loaded = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(loaded != null && loaded.createTag(context, origin).equals(saved), "Forgotten20 native start NBT changed");
|
||||
var expectedRooms = generator.ruinsPlan17().networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z()).flatMap(n -> n.rooms().stream()).toList();
|
||||
var actualRooms = start.getPieces().stream().map(RuinsPiece20.class::cast).map(RuinsPiece20::room).toList();
|
||||
helper.assertTrue(expectedRooms.size() == actualRooms.size() && new HashSet<>(expectedRooms).equals(new HashSet<>(actualRooms)), "Forgotten20 native rooms differ from plan");
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("ruins_starts")) { var item = value.getAsJsonObject(); var at = item.getAsJsonArray("start_chunk"); if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = item; }
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(digest), "Forgotten20 native start changed on a second JVM load");
|
||||
}
|
||||
for (var raw : start.getPieces()) {
|
||||
var piece = (RuinsPiece20) raw; var room = piece.room(); count++;
|
||||
var tag = piece.createTag(context); var roundtrip = new RuinsPiece20(context, tag);
|
||||
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(tag), "Forgotten20 piece lost saved parameters");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> helper.assertTrue(level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true).getReferencesForStructure(registry.value()).contains(origin.pack()), "Forgotten20 chunk lost native references"));
|
||||
var expected = rendered(room); blockCount += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
helper.assertTrue(piece.getBoundingBox().isInside(entry.getKey()), "Forgotten20 renderer escaped saved bounds");
|
||||
helper.assertTrue(level.getBlockState(entry.getKey()).is(entry.getValue().getBlock()), "Forgotten20 native geometry changed at " + entry.getKey());
|
||||
}
|
||||
connections = connections.add(NativeConnections20.verify(helper, expected.blocks(), Set.of()));
|
||||
for (var support : room.supports()) assertRock(helper, new BlockPos(support.x(), support.minY() - 1, support.z()));
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Forgotten20 missing deferred chest");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && chest.seed() == entity.getLootTableSeed(), "Forgotten20 consumed/changed saved loot"); lootCount++;
|
||||
}
|
||||
}
|
||||
summaries.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", digest, "pieces", start.getPieces().size()));
|
||||
}
|
||||
report.put("ruins_starts", summaries); report.put("ruins_native_pieces", count); report.put("ruins_native_blocks_checked", blockCount);
|
||||
report.put("ruins_unopened_deferred_chests", lootCount); report.put("ruins_connections", connections);
|
||||
report.put("ruins_access", generator.ruinsPlan17().networks().stream().map(n -> verifyAccess(helper, n)).toList());
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(HeritagePiece21 piece) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
int x0 = piece.isSite() ? piece.site().minX() : piece.segment().minX();
|
||||
int y0 = piece.isSite() ? piece.site().floorY() : piece.segment().minY();
|
||||
int z0 = piece.isSite() ? piece.site().minZ() : piece.segment().minZ();
|
||||
var writer = new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(new BlockPos(x0 + x, y0 + y, z0 + z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var at = new BlockPos(x0 + x, y0 + y, z0 + z); blocks.put(at, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(at, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
};
|
||||
if (piece.isSite()) HeritageRenderer21.render(piece.site(), writer); else HeritageRenderer21.renderSegment(piece.segment(), writer);
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Rendered rendered(RuinsPlan17.Room room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
RuinsRenderer20.render(room, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
});
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
|
||||
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
|
||||
var hatches = new HashMap<BlockPos, BlockState>();
|
||||
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
|
||||
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
|
||||
hatches.forEach((at, state) -> {
|
||||
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
|
||||
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
});
|
||||
try {
|
||||
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
var start = RuinsPiece20.locator(access); var reached = walk(helper, start, inside);
|
||||
int targets = 0;
|
||||
for (var room : network.rooms()) for (var target : RuinsRenderer20.targets(room)) {
|
||||
var point = at(room, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
|
||||
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
|
||||
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
|
||||
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
private static boolean isOuterWall(HeritagePlan19.Site site, BlockPos pos) {
|
||||
return pos.getY() >= site.floorY() + 2 && pos.getY() <= site.floorY() + 3
|
||||
&& (pos.getX() == site.minX() + 2 || pos.getX() == site.maxX() - 2 || pos.getZ() == site.minZ() + 2 || pos.getZ() == site.maxZ() - 2)
|
||||
&& Math.abs(pos.getX() - site.entry().x()) + Math.abs(pos.getZ() - site.entry().z()) > 6;
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, HeritagePlan19.Site site) {
|
||||
var level = helper.getLevel(); var expected = rendered(new HeritagePiece21(site)); var doors = new HashMap<BlockPos, BlockState>();
|
||||
expected.blocks().forEach((at, state) -> {
|
||||
if ((state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) && state.hasProperty(BlockStateProperties.OPEN)) doors.put(at, level.getBlockState(at));
|
||||
});
|
||||
doors.forEach((at, state) -> level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
try {
|
||||
var entry = HeritagePiece21.locator(site);
|
||||
Predicate<BlockPos> inside = p -> p.equals(entry) || p.getX() >= site.minX() && p.getX() <= site.maxX()
|
||||
&& p.getZ() >= site.minZ() && p.getZ() <= site.maxZ() && p.getY() >= site.floorY() - 4 && p.getY() < site.maxY();
|
||||
var reached = walk(helper, entry, inside); int targets = 0;
|
||||
for (var target : HeritageRenderer21.targets(site)) {
|
||||
var at = new BlockPos(site.minX() + target.x(), site.floorY() + target.y(), site.minZ() + target.z());
|
||||
helper.assertTrue(reached.contains(at), "Site " + site.id() + " cannot reach " + target.name() + " at " + at); targets++;
|
||||
}
|
||||
return Map.of("id", site.id(), "kind", site.kind().name(), "entry", coords(entry), "standing_cells", reached.size(), "walkable_targets", targets);
|
||||
} finally { doors.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static List<Map<String, Object>> verifyLinks(GameTestHelper helper, HeritagePlan19 plan) {
|
||||
helper.assertTrue(!plan.links().isEmpty(), "Reference island needs a real route between accepted destinations");
|
||||
var entries = new HashMap<String, HeritagePlan19.Entry>(); plan.sites().forEach(s -> entries.put(s.id(), s.entry())); plan.landmarks().forEach(s -> entries.put(s.id(), s.entry()));
|
||||
var cells = new HashMap<Long, HeritagePlan19.Cell>(); plan.segments().forEach(s -> s.cells().forEach(c -> cells.put(column(c.x(), c.z()), c)));
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var link : plan.links()) {
|
||||
var from = entries.get(link.fromId()); var to = entries.get(link.toId());
|
||||
helper.assertTrue(from != null && to != null, "Route points to an absent destination");
|
||||
var first = link.path().getFirst(); var last = link.path().getLast();
|
||||
helper.assertTrue(first.x() == from.x() && first.z() == from.z() && first.floorY() + 1 == from.y()
|
||||
&& last.x() == to.x() && last.z() == to.z() && last.floorY() + 1 == to.y(), "Route does not meet its real entrances");
|
||||
int present = 0, missing = 0;
|
||||
for (int i = 0; i < link.path().size(); i++) {
|
||||
var point = link.path().get(i); var at = new BlockPos(point.x(), point.floorY() + 1, point.z());
|
||||
helper.assertTrue(standable(helper.getLevel(), at), "Ballast route " + link.id() + " is blocked at " + at);
|
||||
if (i > 0) { var prior = link.path().get(i - 1); if (point.floorY() > prior.floorY()) helper.assertTrue(clearBody(helper.getLevel(), new BlockPos(prior.x(), prior.floorY() + 2, prior.z())), "Route slope cannot be climbed by a player"); }
|
||||
var cell = cells.get(column(point.x(), point.z()));
|
||||
if (cell != null && cell.rail() != HeritagePlan19.Rail.NONE) {
|
||||
var actual = helper.getLevel().getBlockState(at);
|
||||
if (cell.railPresent()) {
|
||||
helper.assertTrue(actual.is(Blocks.RAIL), "Saved existing rail disappeared");
|
||||
helper.assertTrue(actual.getValue(BlockStateProperties.RAIL_SHAPE).name().equals(cell.rail().name()), "Native rail update changed the intended slope or curve at " + at); present++;
|
||||
} else { helper.assertTrue(!actual.is(Blocks.RAIL), "An intentionally missing rail regenerated"); missing++; }
|
||||
}
|
||||
}
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow() - link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
result.add(Map.of("id", link.id(), "from", link.fromId(), "to", link.toId(), "kind", link.kind().name(), "length", link.path().size(),
|
||||
"span_x", spanX, "span_z", spanZ, "existing_rails", present, "deliberately_missing_rails", missing, "path", pathCoords(link)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static int routeSpan(HeritagePlan19.Link link) {
|
||||
int spanX = link.path().stream().mapToInt(HeritagePlan19.Point::x).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::x).min().orElseThrow();
|
||||
int spanZ = link.path().stream().mapToInt(HeritagePlan19.Point::z).max().orElseThrow()
|
||||
- link.path().stream().mapToInt(HeritagePlan19.Point::z).min().orElseThrow();
|
||||
return Math.max(spanX, spanZ);
|
||||
}
|
||||
private static List<List<Integer>> pathCoords(HeritagePlan19.Link link) {
|
||||
return link.path().stream().map(point -> List.of(point.x(), point.floorY(), point.z())).toList();
|
||||
}
|
||||
private static int verifySiteBearing(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan, HeritagePlan19.Site site) {
|
||||
int checked = 0;
|
||||
// The planner certifies a nine-point bearing grid, either directly below its slab or below a saved short footing.
|
||||
for (int dx : new int[] {1, 8, 15}) for (int dz : new int[] {1, 8, 15}) {
|
||||
int x = site.minX() + dx, z = site.minZ() + dz;
|
||||
var support = site.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
int bottom = support.map(HeritagePlan19.Support::minY).orElse(site.minY());
|
||||
for (int below = 1; below <= 2; below++) {
|
||||
var at = new BlockPos(x, bottom - below, z); assertRock(helper, at);
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Site's certified bearing layer is not published in the decoration mask: " + site.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(at.getX(), at.getY(), at.getZ()), "Original bearing rock became a writable heritage volume: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
private static List<Map<String, Object>> verifyReadOnlyLandings(GameTestHelper helper, SanctuaryChunkGenerator generator, HeritagePlan19 plan) {
|
||||
var used = new HashSet<String>(); plan.links().forEach(link -> { used.add(link.fromId()); used.add(link.toId()); });
|
||||
var result = new ArrayList<Map<String, Object>>();
|
||||
for (var landmark : plan.landmarks()) if (used.contains(landmark.id())) {
|
||||
var e = landmark.entry(); int checked = 0;
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = e.x() + ((e.facing() & 1) == 0 ? side : 0), z = e.z() + ((e.facing() & 1) == 1 ? side : 0);
|
||||
for (int y = e.y() - 2; y <= e.y() + 2; y++) {
|
||||
var at = new BlockPos(x, y, z);
|
||||
helper.assertTrue(plan.protects(x, y, z) && generator.protectsCityDecoration(at), "Read-only arrival lost its decoration mask: " + landmark.id() + " at " + at);
|
||||
helper.assertTrue(!plan.contains(x, y, z), "Heritage gained write ownership of a historical arrival: " + landmark.id() + " at " + at);
|
||||
if (y == e.y() - 1) assertRock(helper, at);
|
||||
if (y >= e.y()) helper.assertTrue(helper.getLevel().getBlockState(at).isAir(), "Decoration obstructed a certified historical arrival: " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
result.add(Map.of("id", landmark.id(), "entry", List.of(e.x(), e.y(), e.z()), "read_only_cells", checked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static void assertRock(GameTestHelper helper, BlockPos at) {
|
||||
helper.assertTrue(helper.getLevel().getBlockState(at).isSolid() && helper.getLevel().getFluidState(at).isEmpty(), "Lost original rock bearing at " + at);
|
||||
}
|
||||
private static void verifyLootTables(GameTestHelper helper, Map<String, Object> report) {
|
||||
var result = new ArrayList<Map<String, Object>>(); var level = helper.getLevel();
|
||||
var params = new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, new Vec3(0, 160, 0)).create(LootContextParamSets.CHEST);
|
||||
for (var kind : HeritagePlan19.Kind.values()) {
|
||||
var table = level.getServer().reloadableRegistries().getLootTable(lootKey(kind));
|
||||
helper.assertTrue(table != LootTable.EMPTY, "Heritage tool table did not load: " + kind);
|
||||
for (long seed : new long[] {0, 42, -1}) helper.assertTrue(table.getRandomItems(params, seed).stream().anyMatch(stack -> stack.is(tool(kind))), "Tool table is empty or lacks its expected primary tool: " + kind);
|
||||
result.add(Map.of("kind", kind.name(), "table", lootKey(kind).identifier().toString(), "sampled_seeds", List.of(0, 42, -1)));
|
||||
}
|
||||
report.put("tool_tables_sampled_separately", result);
|
||||
}
|
||||
private static ResourceKey<LootTable> lootKey(HeritagePlan19.Kind kind) { return ResourceKey.create(Registries.LOOT_TABLE, SanctuaryMod.id("chests/heritage19/" + kind.name().toLowerCase(Locale.ROOT))); }
|
||||
private static Item tool(HeritagePlan19.Kind kind) {
|
||||
return switch (kind) {
|
||||
case OBSERVATORY -> Items.SPYGLASS;
|
||||
case POST_OFFICE, EXPEDITION_OFFICE, RADIO_RELAY -> Items.COMPASS;
|
||||
case SUBSTATION, IMPOSSIBLE_SERVICE -> Items.CLOCK;
|
||||
case FREIGHT_STATION -> Items.MINECART;
|
||||
case COMMUNAL_DEPOT -> Items.IRON_AXE;
|
||||
case RAIL_WORKSHOP -> Items.IRON_PICKAXE;
|
||||
case PUMP_STATION -> Items.BUCKET;
|
||||
case GREENHOUSE -> Items.IRON_HOE;
|
||||
case VOID_DOCK -> Items.LEAD;
|
||||
case PHOTO_CLUB -> Items.BOOK;
|
||||
case CONSTRUCTION_REFUGE -> Items.IRON_SHOVEL;
|
||||
};
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked or unsupported exterior entrance: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER))
|
||||
&& standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native site traversal escaped its bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below(); return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs()) if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static long column(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("heritage21-create-proof.json"); }
|
||||
private static String planHash(HeritagePlan19 plan) { return hash(plan.sites().toString() + plan.landmarks() + plan.links() + plan.segments()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "heritage21-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n"; Files.writeString(path, value); if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
-448
@@ -1,448 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.HydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.ShoreSugarCaneFeature;
|
||||
import fr.koka.sanctuary.worldgen.SurfaceHydrology;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/** Checks the actual decorated terrain and fluid simulation in a disposable world, far from test fixtures. */
|
||||
public final class HydrologyDiagnostics {
|
||||
private static final List<Direction> HORIZONTAL = List.of(Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST);
|
||||
|
||||
private HydrologyDiagnostics() {}
|
||||
|
||||
public static void start(GameTestHelper helper) throws IOException {
|
||||
Context context = new Context(helper);
|
||||
context.prepare();
|
||||
context.verifyNaturalShellAndFinishedWater();
|
||||
context.verifyAllObservedWaterIsPlanned();
|
||||
context.export("generated");
|
||||
helper.assertTrue(context.caneBlocks > 0,
|
||||
"Regression seed must provide at least one naturally valid shore sugar cane plant; inspect the exported site map");
|
||||
// Other synchronous generation diagnostics finish before this tick. The temporary fluid
|
||||
// witness therefore cannot be counted by the initial resource survey.
|
||||
helper.runAtTickTime(20, context::startFluidWitness);
|
||||
helper.runAtTickTime(50, context::verifyFluidWitnessAndRemoveIt);
|
||||
helper.runAtTickTime(200, () -> {
|
||||
try {
|
||||
context.verifyNaturalShellAndFinishedWater();
|
||||
context.verifyAllObservedWaterIsPlanned();
|
||||
context.export("after-200-ticks");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: {} water blocks retained after real fluid ticks, {} chunk seams checked",
|
||||
context.level.getSeed(), context.expectedWater.size(), context.seams);
|
||||
helper.succeed();
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Could not write hydrology diagnostics", exception);
|
||||
} finally {
|
||||
context.cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void verifyLegacyIsolation(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var active = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(HydrologyRuntime.enabled(active), "The current preset must enable surface hydrology");
|
||||
helper.assertTrue(active.generatorSettings().is(SanctuarySpawn.HYDROLOGY_SETTINGS),
|
||||
"The active generator must use the new hydrology settings, not alter a legacy settings key");
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
for (var key : List.of(SanctuarySpawn.SETTINGS, SanctuarySpawn.RAISED_SETTINGS)) {
|
||||
var legacy = new NoiseBasedChunkGenerator(active.getBiomeSource(), registry.getOrThrow(key));
|
||||
helper.assertFalse(HydrologyRuntime.enabled(legacy), "Saved legacy generator must remain dry: " + key.identifier());
|
||||
helper.assertFalse(new ShoreSugarCaneFeature().place(level, legacy, RandomSource.create(0), BlockPos.ZERO),
|
||||
"Even an explicitly invoked shoreline feature must ignore legacy generators");
|
||||
}
|
||||
var biomes = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
var oldBiome = biomes.getOrThrow(ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest"))).value();
|
||||
var wetBiome = biomes.getOrThrow(ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest_hydrology"))).value();
|
||||
var shore = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getOrThrow(
|
||||
ResourceKey.create(Registries.PLACED_FEATURE, SanctuaryMod.id("shore_sugar_cane"))).value();
|
||||
helper.assertFalse(oldBiome.getGenerationSettings().hasFeature(shore),
|
||||
"The old biome referenced by saved worlds must not acquire the new shoreline decoration");
|
||||
helper.assertTrue(wetBiome.getGenerationSettings().hasFeature(shore),
|
||||
"The new hydrology biome must include its shoreline decoration");
|
||||
helper.assertTrue(active.getBiomeSource().possibleBiomes().stream().allMatch(holder -> holder.is(
|
||||
ResourceKey.create(Registries.BIOME, SanctuaryMod.id("starter_forest_hydrology")))),
|
||||
"The current preset must select the separate hydrology biome");
|
||||
}
|
||||
|
||||
private static final class Context {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SurfaceHydrology.Plan plan;
|
||||
final RandomState random;
|
||||
final DensityFunction natural;
|
||||
final Map<Long, SurfaceHydrology.Cell> cells = new HashMap<>();
|
||||
final Set<BlockPos> expectedWater = new HashSet<>();
|
||||
final Map<Long, LevelChunk> chunks = new TreeMap<>();
|
||||
final Set<Long> newlyForced = new HashSet<>();
|
||||
BlockPos witness;
|
||||
boolean witnessPlaced;
|
||||
boolean allWaterChunksTicking;
|
||||
int seams;
|
||||
int caneBlocks;
|
||||
|
||||
Context(GameTestHelper helper) {
|
||||
this.helper = helper;
|
||||
level = helper.getLevel();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(HydrologyRuntime.enabled(generator), "Hydrology test requires the production wet preset");
|
||||
plan = HydrologyRuntime.plan(level);
|
||||
random = level.getChunkSource().randomState();
|
||||
natural = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
}
|
||||
|
||||
void prepare() {
|
||||
helper.assertTrue(!plan.features().isEmpty(), "Regression seed must exercise real surface water");
|
||||
for (var feature : plan.features()) {
|
||||
SanctuaryMod.LOGGER.info("Hydrology planned site: seed {}, {}, Y{}, start {}, {} water columns, {} path points",
|
||||
level.getSeed(), feature.kind(), feature.waterY(), feature.path().getFirst(), feature.waterCells(), feature.path().size());
|
||||
}
|
||||
if (level.getSeed() == 0L) {
|
||||
helper.assertTrue(plan.features().stream().anyMatch(feature -> feature.kind() == SurfaceHydrology.Kind.STREAM),
|
||||
"The reference seed 0 must demonstrate a real stream, not only isolated ponds; see the logged planned sites");
|
||||
}
|
||||
Set<Long> requested = new HashSet<>();
|
||||
for (var cell : plan.cells()) {
|
||||
helper.assertTrue(cells.put(ChunkPos.pack(cell.x(), cell.z()), cell) == null,
|
||||
"A plan must own each horizontal column exactly once");
|
||||
if (cell.hasWater()) {
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
expectedWater.add(new BlockPos(cell.x(), y, cell.z()));
|
||||
}
|
||||
}
|
||||
int cx = cell.x() >> 4;
|
||||
int cz = cell.z() >> 4;
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) requested.add(ChunkPos.pack(cx + dx, cz + dz));
|
||||
}
|
||||
}
|
||||
helper.assertTrue(!expectedWater.isEmpty(), "At least one generated water volume must be tested");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: preparing {} site and neighbour chunks for actual water inspection",
|
||||
level.getSeed(), requested.size());
|
||||
for (long packed : requested.stream().sorted().toList()) {
|
||||
ChunkPos pos = ChunkPos.unpack(packed);
|
||||
if (level.setChunkForced(pos.x(), pos.z(), true)) newlyForced.add(packed);
|
||||
var chunk = level.getChunkSource().getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Hydrology inspection requires a decorated FULL chunk");
|
||||
chunks.put(packed, (LevelChunk) chunk);
|
||||
}
|
||||
// Use one contained witness in the SAME simulation chunk as actual water. It proves
|
||||
// that the island's fluids tick, rather than merely waiting beside the remote test grid.
|
||||
BlockPos first = expectedWater.stream().min(Comparator.comparingInt((BlockPos pos) -> pos.getX())
|
||||
.thenComparingInt(BlockPos::getZ).thenComparingInt(BlockPos::getY)).orElseThrow();
|
||||
witness = new BlockPos((first.getX() >> 4) * 16 + 8, level.getMaxY() - 20,
|
||||
(first.getZ() >> 4) * 16 + 8);
|
||||
for (BlockPos position : expectedWater) {
|
||||
for (Direction direction : List.of(Direction.EAST, Direction.SOUTH)) {
|
||||
BlockPos next = position.relative(direction);
|
||||
if (expectedWater.contains(next)
|
||||
&& (position.getX() >> 4 != next.getX() >> 4 || position.getZ() >> 4 != next.getZ() >> 4)) seams++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(seams > 0, "Regression seed must exercise a continuous water volume across a chunk seam");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: {} features, {} water blocks, {} FULL chunks including halo, {} seam adjacencies",
|
||||
level.getSeed(), plan.features().size(), expectedWater.size(), chunks.size(), seams);
|
||||
}
|
||||
|
||||
void verifyNaturalShellAndFinishedWater() {
|
||||
for (var cell : plan.cells()) {
|
||||
if (!cell.hasWater()) continue;
|
||||
helper.assertTrue(cell.waterY() > cell.bedY() && cell.waterY() - cell.bedY() <= SurfaceHydrology.MAX_DEPTH,
|
||||
"A surface basin must keep the declared shallow water depth");
|
||||
helper.assertTrue(cell.carveTop() - cell.bedY() <= SurfaceHydrology.MAX_CARVE,
|
||||
"Hydrology must not excavate a deep artificial chamber beneath the surface");
|
||||
for (int depth = 0; depth < 3; depth++) {
|
||||
int y = cell.bedY() - depth;
|
||||
helper.assertTrue(sample(cell.x(), y, cell.z()) > 0,
|
||||
"Water must retain three blocks of natural floor, without adding a liner at " + cell.x() + "," + y + "," + cell.z());
|
||||
BlockPos floor = new BlockPos(cell.x(), y, cell.z());
|
||||
helper.assertTrue(level.getBlockState(floor).isCollisionShapeFullBlock(level, floor),
|
||||
"Generated water floor must remain solid after decoration and fluid ticks: " + floor);
|
||||
}
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
|
||||
BlockPos water = new BlockPos(cell.x(), y, cell.z());
|
||||
helper.assertTrue(level.getFluidState(water).is(FluidTags.WATER) && level.getFluidState(water).isSource(),
|
||||
"The planned water volume must exist as retained source water: " + water);
|
||||
for (Direction direction : HORIZONTAL) {
|
||||
BlockPos side = water.relative(direction);
|
||||
if (expectedWater.contains(side)) continue;
|
||||
helper.assertTrue(sample(side.getX(), side.getY(), side.getZ()) > 0,
|
||||
"Every external underwater side must be supported by natural terrain: " + side);
|
||||
helper.assertTrue(level.getBlockState(side).isCollisionShapeFullBlock(level, side),
|
||||
"A decorated shore must not leave a hole in the three-dimensional water shell: " + side);
|
||||
}
|
||||
}
|
||||
// The original density must have no roof above the explicitly carved surface.
|
||||
// Leaves and branches from neighbouring trees may shade the water afterwards.
|
||||
for (int y = cell.carveTop() + 1; y <= level.getMaxY(); y++) {
|
||||
helper.assertTrue(sample(cell.x(), y, cell.z()) <= 0,
|
||||
"A surface basin must not be a flooded underground chamber: " + cell.x() + "," + y + "," + cell.z());
|
||||
}
|
||||
}
|
||||
for (var feature : plan.features()) {
|
||||
if (feature.kind() != SurfaceHydrology.Kind.STREAM) continue;
|
||||
helper.assertTrue(feature.path().size() > 1, "A stream needs a route connecting distinct surface locations");
|
||||
double travelled = 0;
|
||||
for (int i = 1; i < feature.path().size(); i++) {
|
||||
var from = feature.path().get(i - 1);
|
||||
var to = feature.path().get(i);
|
||||
int length = Math.max(Math.abs(to.x() - from.x()), Math.abs(to.z() - from.z()));
|
||||
helper.assertTrue(length > 0, "A stream route must advance between consecutive points");
|
||||
helper.assertTrue(from.x() == to.x() || from.z() == to.z()
|
||||
|| Math.abs(to.x() - from.x()) == Math.abs(to.z() - from.z()),
|
||||
"Stream control points must be joined by cardinal or diagonal steps");
|
||||
travelled += Math.hypot(to.x() - from.x(), to.z() - from.z());
|
||||
BlockPos previous = null;
|
||||
for (int step = 0; step <= length; step++) {
|
||||
BlockPos p = new BlockPos(from.x() + (to.x() - from.x()) * step / length,
|
||||
feature.waterY(), from.z() + (to.z() - from.z()) * step / length);
|
||||
helper.assertTrue(expectedWater.contains(p) && level.getFluidState(p).is(FluidTags.WATER),
|
||||
"The real stream must form an uninterrupted water route across chunk boundaries: " + p);
|
||||
var column = plan.cellAt(p.getX(), p.getZ());
|
||||
helper.assertTrue(column != null && column.waterY() == feature.waterY(),
|
||||
"This stream contract has one constant water level along its whole route");
|
||||
if (previous != null && p.getX() != previous.getX() && p.getZ() != previous.getZ()) {
|
||||
BlockPos bridgeX = new BlockPos(p.getX(), p.getY(), previous.getZ());
|
||||
BlockPos bridgeZ = new BlockPos(previous.getX(), p.getY(), p.getZ());
|
||||
helper.assertTrue(expectedWater.contains(bridgeX) && level.getFluidState(bridgeX).is(FluidTags.WATER)
|
||||
|| expectedWater.contains(bridgeZ) && level.getFluidState(bridgeZ).is(FluidTags.WATER),
|
||||
"Diagonal route steps need actual cardinal water connectivity, not only touching corners");
|
||||
}
|
||||
previous = p;
|
||||
}
|
||||
}
|
||||
var first = feature.path().getFirst();
|
||||
var last = feature.path().getLast();
|
||||
helper.assertTrue(travelled >= 24 && Math.hypot(last.x() - first.x(), last.z() - first.z()) >= 16,
|
||||
"A demonstrated stream must have at least 24 blocks of route between locations at least 16 blocks apart");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyAllObservedWaterIsPlanned() {
|
||||
int[] observed = {0};
|
||||
int[] cane = {0};
|
||||
for (LevelChunk chunk : chunks.values()) {
|
||||
chunk.findBlocks(state -> state.getFluidState().is(FluidTags.WATER) || state.is(Blocks.SUGAR_CANE), (pos, state) -> {
|
||||
if (state.getFluidState().is(FluidTags.WATER)) {
|
||||
helper.assertTrue(expectedWater.contains(pos),
|
||||
"Water escaped its declared surface volume, possibly through a chunk seam or toward the void: " + pos);
|
||||
observed[0]++;
|
||||
}
|
||||
if (state.is(Blocks.SUGAR_CANE)) {
|
||||
helper.assertTrue(state.canSurvive(level, pos), "Generated shore sugar cane must survive at " + pos);
|
||||
cane[0]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
helper.assertTrue(observed[0] == expectedWater.size(), "Observed water must account for every planned water block");
|
||||
caneBlocks = cane[0];
|
||||
}
|
||||
|
||||
void startFluidWitness() {
|
||||
helper.assertTrue(level.shouldTickBlocksAt(witness), "The witness and its water-site chunk must actually tick");
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) {
|
||||
BlockPos p = witness.offset(dx, dy, dz);
|
||||
helper.assertTrue(level.getBlockState(p).isAir(), "The temporary simulation witness must use empty test space");
|
||||
}
|
||||
}
|
||||
}
|
||||
witnessPlaced = true;
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) {
|
||||
if (dx != 0 || dz != 0 || dy == -4) level.setBlock(witness.offset(dx, dy, dz), Blocks.STONE.defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
level.setBlock(witness, Blocks.WATER.defaultBlockState(), 3);
|
||||
helper.assertTrue(level.getFluidState(witness.below()).isEmpty(), "Witness starts with an empty cell below its source");
|
||||
level.scheduleTick(witness, Fluids.WATER, 1);
|
||||
for (BlockPos water : expectedWater) {
|
||||
helper.assertTrue(level.shouldTickBlocksAt(water), "Every actual water-site chunk must be simulated: " + water);
|
||||
level.scheduleTick(water, Fluids.WATER, 1);
|
||||
}
|
||||
allWaterChunksTicking = true;
|
||||
}
|
||||
|
||||
void verifyFluidWitnessAndRemoveIt() {
|
||||
try {
|
||||
helper.assertTrue(level.getFluidState(witness.below()).is(FluidTags.WATER),
|
||||
"Real fluid simulation must move the witness water down inside the forced island chunk");
|
||||
SanctuaryMod.LOGGER.info("Hydrology seed {}: fluid tick witness passed at {}", level.getSeed(), witness);
|
||||
} finally {
|
||||
removeWitness();
|
||||
}
|
||||
}
|
||||
|
||||
void removeWitness() {
|
||||
if (!witnessPlaced) return;
|
||||
// Empty the fluid column before opening its walls, keeping the test fixture contained.
|
||||
for (int dy = 0; dy >= -3; dy--) level.setBlock(witness.offset(0, dy, 0), Blocks.AIR.defaultBlockState(), 3);
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
for (int dy = -4; dy <= 0; dy++) level.setBlock(witness.offset(dx, dy, dz), Blocks.AIR.defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
witnessPlaced = false;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
removeWitness();
|
||||
for (long packed : newlyForced) {
|
||||
ChunkPos chunk = ChunkPos.unpack(packed);
|
||||
level.setChunkForced(chunk.x(), chunk.z(), false);
|
||||
}
|
||||
}
|
||||
|
||||
float sample(int x, int y, int z) {
|
||||
return random.sampleBlockValueUncached(natural, x, y, z);
|
||||
}
|
||||
|
||||
void export(String phase) throws IOException {
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
String stem = "surface-hydrology-seed-" + level.getSeed() + "-" + phase;
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("phase", phase);
|
||||
report.put("actual_water_blocks", expectedWater.size());
|
||||
report.put("actual_cane_blocks", caneBlocks);
|
||||
report.put("checked_chunk_seam_adjacencies", seams);
|
||||
report.put("all_water_site_chunks_confirmed_ticking", allWaterChunksTicking);
|
||||
report.put("inspected_full_chunks", chunks.values().stream().map(chunk ->
|
||||
Map.of("x", chunk.getPos().x(), "z", chunk.getPos().z())).toList());
|
||||
report.put("features", plan.features());
|
||||
report.put("shore_diagnostics_by_feature", shoreDiagnostics());
|
||||
report.put("planned_columns", plan.cells());
|
||||
report.put("scope", "actual blocks in completed site chunks and a one-chunk halo; uninspected map cells remain blank");
|
||||
Files.writeString(directory.resolve(stem + ".json"), new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
exportMap(directory.resolve(stem + ".png"), phase);
|
||||
}
|
||||
|
||||
Map<Long, Map<String, Object>> shoreDiagnostics() {
|
||||
Map<Long, Map<String, Object>> result = new LinkedHashMap<>();
|
||||
for (var feature : plan.features()) {
|
||||
int dryBanks = 0;
|
||||
int banksAtWaterLevel = 0;
|
||||
int baseCanSurvive = 0;
|
||||
int unobstructedBases = 0;
|
||||
int viableUnobstructedBases = 0;
|
||||
int actualCane = 0;
|
||||
for (var cell : plan.cells()) {
|
||||
if (cell.featureId() != feature.id() || cell.hasWater()) continue;
|
||||
dryBanks++;
|
||||
if (cell.bedY() == feature.waterY()) banksAtWaterLevel++;
|
||||
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
|
||||
boolean viable = Blocks.SUGAR_CANE.defaultBlockState().canSurvive(level, base);
|
||||
boolean clear = level.getBlockState(base).isAir() || level.getBlockState(base).is(Blocks.SUGAR_CANE);
|
||||
if (viable) baseCanSurvive++;
|
||||
if (clear) unobstructedBases++;
|
||||
if (viable && clear) viableUnobstructedBases++;
|
||||
for (int height = 0; height < 4; height++) {
|
||||
if (level.getBlockState(base.above(height)).is(Blocks.SUGAR_CANE)) actualCane++;
|
||||
else break;
|
||||
}
|
||||
}
|
||||
result.put(feature.id(), Map.of("kind", feature.kind().name(), "water_y", feature.waterY(),
|
||||
"dry_bank_columns", dryBanks, "bank_surface_at_water_level", banksAtWaterLevel,
|
||||
"bases_where_cane_can_survive", baseCanSurvive, "unobstructed_bases", unobstructedBases,
|
||||
"viable_unobstructed_bases", viableUnobstructedBases,
|
||||
"actual_cane_blocks_on_planned_banks", actualCane));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void exportMap(Path output, String phase) throws IOException {
|
||||
int minX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockX()).min().orElseThrow();
|
||||
int maxX = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockX()).max().orElseThrow();
|
||||
int minZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMinBlockZ()).min().orElseThrow();
|
||||
int maxZ = chunks.values().stream().mapToInt(chunk -> chunk.getPos().getMaxBlockZ()).max().orElseThrow();
|
||||
int scale = Math.max(1, Math.min(3, 1000 / Math.max(maxX - minX + 1, maxZ - minZ + 1)));
|
||||
int width = (maxX - minX + 1) * scale;
|
||||
int height = (maxZ - minZ + 1) * scale;
|
||||
BufferedImage image = new BufferedImage(Math.max(width + 40, 920), height + 110, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
graphics.setColor(new Color(0xF0F0EC));
|
||||
graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
graphics.setColor(new Color(0x17222C));
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 17));
|
||||
graphics.drawString("Actual FULL chunks | seed " + level.getSeed() + " | " + phase, 20, 24);
|
||||
graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
|
||||
graphics.drawString("Water blue; sand gold; clay teal; gravel brown; cane bright green; terrain height grey. Canopy skipped.", 20, 45);
|
||||
graphics.drawString("Not a Minecraft render. X=" + minX + ".." + maxX + ", Z=" + minZ + ".." + maxZ
|
||||
+ ". Dark blue = void; dark brown = not inspected; faint lines = chunk seams.", 20, 63);
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
|
||||
int color = 0x3A3030;
|
||||
if (chunk != null) {
|
||||
color = 0x142738;
|
||||
for (int y = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); y >= level.getMinY(); y--) {
|
||||
position.set(x, y, z);
|
||||
BlockState state = chunk.getBlockState(position);
|
||||
if (state.getFluidState().is(FluidTags.WATER)) { color = 0x207BD4; break; }
|
||||
if (state.is(Blocks.SUGAR_CANE)) { color = 0x7AFF38; break; }
|
||||
if (state.is(Blocks.SAND)) { color = 0xD5B668; break; }
|
||||
if (state.is(Blocks.CLAY)) { color = 0x6D9FAD; break; }
|
||||
if (state.is(Blocks.GRAVEL)) { color = 0x9B8878; break; }
|
||||
if (state.isAir() || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS)
|
||||
|| state.getCollisionShape(level, position).isEmpty()) continue;
|
||||
int value = 40 + Math.clamp(y, 0, level.getHeight()) * 180 / level.getHeight();
|
||||
color = value << 16 | value << 8 | value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
graphics.setColor(new Color(color));
|
||||
graphics.fillRect(20 + (x - minX) * scale, 80 + (z - minZ) * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
graphics.setColor(new Color(255, 255, 255, 35));
|
||||
for (int x = minX; x <= maxX; x += 16) graphics.drawLine(20 + (x - minX) * scale, 80, 20 + (x - minX) * scale, 80 + height);
|
||||
for (int z = minZ; z <= maxZ; z += 16) graphics.drawLine(20, 80 + (z - minZ) * scale, 20 + width, 80 + (z - minZ) * scale);
|
||||
graphics.dispose();
|
||||
ImageIO.write(image, "png", output.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
-1308
File diff suppressed because it is too large
Load Diff
-911
@@ -1,911 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.expansion.ExpansionJournal;
|
||||
import fr.koka.sanctuary.expansion.ExpansionRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityInteriors15;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPiece15;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityStructures;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCitySurface15;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCitySupportPiece15;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground15;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUndergroundPiece15;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in generation-15 proof: bounded natural prospects, native placement, fluid ticks and a second JVM.
|
||||
* It never imports an artificial city into the natural-coverage totals or opens a personal world. */
|
||||
public final class LostCity15WorldGameTests {
|
||||
private static final Set<LostCityUnderground15.Kind> REQUIRED_UNDERGROUND = Set.of(
|
||||
LostCityUnderground15.Kind.SEWER, LostCityUnderground15.Kind.POOLROOM, LostCityUnderground15.Kind.GARAGE);
|
||||
private static final Set<LostCityPlan.Kind> SURFACE_INTEREST = Set.of(
|
||||
LostCityPlan.Kind.BRIDGE, LostCityPlan.Kind.POOL, LostCityPlan.Kind.PARKING);
|
||||
private static final int MAX_DISTRICTS_PER_ISLAND = 6;
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void cityFragmentsAndUtilitiesPersistNatively(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"Lost City15 requires the unified generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 15, "Lost City15 must use a fresh generation-15 world");
|
||||
if (Boolean.getBoolean("sanctuary.test.expansionReload")) reload(helper, generator);
|
||||
else new Scenario(helper, generator).advance();
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final ExpansionRuntime.Session session;
|
||||
final Map<String, Object> report;
|
||||
final List<Map<String, Object>> sites = new ArrayList<>();
|
||||
final List<Map<String, Object>> prospects = new ArrayList<>();
|
||||
final List<Map<String, Object>> naturalPlans = new ArrayList<>();
|
||||
final List<LostCityUnderground15.Module> wetModules = new ArrayList<>();
|
||||
final List<LostCityPlan.Parcel> wetSurfacePools = new ArrayList<>();
|
||||
final Set<LostCityUnderground15.Kind> observedKinds = new HashSet<>();
|
||||
final Set<LostCityPlan.Kind> observedSurfaceKinds = new HashSet<>();
|
||||
final Set<ChunkPos> forced = new HashSet<>();
|
||||
final List<BlockPos> tickWitnesses = new ArrayList<>();
|
||||
int phase, round;
|
||||
String childId;
|
||||
long readinessWaitTick, simulationStartWorldTick = -1;
|
||||
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator;
|
||||
session = ExpansionRuntime.session(level); report = report(level);
|
||||
report.put("sites", sites); report.put("city_prospects", prospects); report.put("natural_plans", naturalPlans);
|
||||
}
|
||||
|
||||
void advance() {
|
||||
try {
|
||||
if (phase == 0) {
|
||||
verifyLegacy(helper, generator);
|
||||
report.put("saved_interior_models", verifyInteriorModels(helper));
|
||||
observeIsland(session.island("sanctuary"));
|
||||
// Always exercise one actual expansion; a second is reserved for missing natural utility types.
|
||||
childId = prospect();
|
||||
helper.assertTrue(childId != null, "The bounded natural prospects found no city with a building; see city_prospects");
|
||||
session.create(childId, "sanctuary", "east", "lost_city", 256, "natural", 2048);
|
||||
phase = 1;
|
||||
}
|
||||
if (phase == 1 && session.island(childId).status().equals("ready")) {
|
||||
observeIsland(session.island(childId));
|
||||
if (!observedKinds.containsAll(REQUIRED_UNDERGROUND) && round < 2) {
|
||||
childId = prospect();
|
||||
helper.assertTrue(childId != null, "Second bounded search cannot supply a natural city reference");
|
||||
session.create(childId, "sanctuary", "east", "lost_city", 256, "natural", 2048);
|
||||
} else {
|
||||
helper.assertTrue(observedKinds.containsAll(REQUIRED_UNDERGROUND),
|
||||
"Natural reference coverage must include sewer, poolroom and garage; found " + observedKinds);
|
||||
helper.assertTrue(sites.size() >= 2, "The reference must inspect several distinct native district starts");
|
||||
for (var module : wetModules) {
|
||||
forceBounds(LostCityUndergroundPiece15.bounds(module));
|
||||
}
|
||||
for (var pool : wetSurfacePools) {
|
||||
forceBounds(new LostCityPiece15(pool).getBoundingBox());
|
||||
}
|
||||
readinessWaitTick = helper.getTick(); phase = 2;
|
||||
}
|
||||
}
|
||||
if (phase == 2 && simulationStartWorldTick < 0) awaitFluidReadiness();
|
||||
if (phase == 2 && simulationStartWorldTick >= 0 && level.getGameTime() - simulationStartWorldTick >= 120) {
|
||||
verifyFluidWitnesses();
|
||||
report.put("natural_fluid_checks", wetModules.stream().map(m -> verifyWater(helper, m, true)).toList());
|
||||
report.put("natural_surface_pool_checks", wetSurfacePools.stream().map(p -> verifySurfacePool(helper, p, true)).toList());
|
||||
removeFluidWitnesses();
|
||||
var witnessSite = sites.stream().filter(site -> site.containsKey("edit_position")).findFirst().orElseThrow();
|
||||
@SuppressWarnings("unchecked") var at = (List<Integer>) witnessSite.get("edit_position");
|
||||
var gold = new BlockPos(at.get(0), at.get(1), at.get(2));
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", coords(gold));
|
||||
report.put("islands", ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, session.islands()).getOrThrow());
|
||||
report.put("generator_codec", ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow());
|
||||
report.put("journal_sha256", hash(Files.readString(journal(level, generator))));
|
||||
releaseForced();
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush all native starts, utility bodies, supports, loot and gameplay edit");
|
||||
report.put("world_flushed_before_shutdown", true); report.put("passed", true);
|
||||
write(level, report, false); helper.succeed(); return;
|
||||
}
|
||||
if (helper.getTick() % 200 == 0) SanctuaryMod.LOGGER.info("Lost City15 integration phase {}: {}", phase, session.progress());
|
||||
helper.runAtTickTime(helper.getTick() + 10, this::advance);
|
||||
} catch (Exception error) {
|
||||
removeFluidWitnesses();
|
||||
releaseForced(); report.put("passed", false); report.put("failure", error.toString());
|
||||
try { write(level, report, false); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Lost City15 native creation failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
void releaseForced() {
|
||||
for (var pos : forced) level.setChunkForced(pos.x(), pos.z(), false);
|
||||
forced.clear();
|
||||
}
|
||||
void forceBounds(BoundingBox bounds) {
|
||||
bounds.intersectingChunks().forEach(pos -> { level.setChunkForced(pos.x(), pos.z(), true); forced.add(pos); });
|
||||
}
|
||||
|
||||
void awaitFluidReadiness() throws IOException {
|
||||
var pending = forced.stream().filter(pos -> !level.isPositionTickingWithEntitiesLoaded(pos.pack())).toList();
|
||||
report.put("stage", "waiting_for_native_fluid_scheduler"); report.put("fluid_game_time", level.getGameTime());
|
||||
if (!pending.isEmpty()) {
|
||||
report.put("pending_fluid_chunks", pending.stream().map(pos -> Map.of("chunk", List.of(pos.x(), pos.z()),
|
||||
"ticket", level.shouldTickBlocksAt(pos.pack()), "entities", level.areEntitiesLoaded(pos.pack()),
|
||||
"ticking_future", level.getChunkSource().isPositionTicking(pos.pack()))).toList());
|
||||
helper.assertTrue(helper.getTick() - readinessWaitTick < 800,
|
||||
"Forced natural water chunks did not become scheduler-ready within800ticks: " + pending);
|
||||
if (helper.getTick() % 100 == 0) {
|
||||
SanctuaryMod.LOGGER.info("Lost City15 waiting for actual fluid scheduler: {} pending chunks at world time {}", pending.size(), level.getGameTime());
|
||||
write(level, report, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
report.remove("pending_fluid_chunks");
|
||||
// Establish retention before simulation, so a pre-existing leak cannot masquerade as a scheduling delay.
|
||||
report.put("natural_water_before_simulation", wetModules.stream().map(m -> verifyWater(helper, m, false)).toList());
|
||||
for (var p : wetSurfacePools) verifySurfacePool(helper, p, false);
|
||||
for (var pos : forced.stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList()) placeFluidWitness(pos);
|
||||
for (var module : wetModules) for (var water : expectedWater(module)) level.scheduleTick(water, Fluids.WATER, 1);
|
||||
for (var pool : wetSurfacePools) for (var water : expectedSurfaceWater(pool)) level.scheduleTick(water, Fluids.WATER, 1);
|
||||
simulationStartWorldTick = level.getGameTime();
|
||||
report.put("stage", "observed_native_fluid_simulation"); report.put("fluid_simulation_start_game_time", simulationStartWorldTick);
|
||||
report.put("temporary_scheduler_witnesses", tickWitnesses.size()); write(level, report, false);
|
||||
}
|
||||
|
||||
void placeFluidWitness(ChunkPos chunk) {
|
||||
BlockPos witness = null;
|
||||
for (int localX : new int[]{4, 8, 12}) for (int localZ : new int[]{4, 8, 12}) {
|
||||
var candidate = new BlockPos(chunk.x() * 16 + localX, 378, chunk.z() * 16 + localZ);
|
||||
boolean empty = true;
|
||||
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) for (int y = -4; y <= 0; y++)
|
||||
empty &= level.getBlockState(candidate.offset(x, y, z)).isAir();
|
||||
if (empty && witness == null) witness = candidate;
|
||||
}
|
||||
helper.assertTrue(witness != null, "The independent fluid witness needs empty development sky in " + chunk);
|
||||
tickWitnesses.add(witness);
|
||||
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) for (int y = -4; y <= 0; y++)
|
||||
if (x != 0 || z != 0 || y == -4) level.setBlock(witness.offset(x, y, z), Blocks.STONE.defaultBlockState(), 3);
|
||||
helper.assertTrue(level.getFluidState(witness.below()).isEmpty(), "Independent witness starts with an empty cell below its source");
|
||||
level.setBlock(witness, Blocks.WATER.defaultBlockState(), 3); level.scheduleTick(witness, Fluids.WATER, 1);
|
||||
}
|
||||
|
||||
void verifyFluidWitnesses() {
|
||||
helper.assertTrue(forced.stream().allMatch(pos -> level.isPositionTickingWithEntitiesLoaded(pos.pack())),
|
||||
"Every observed natural water chunk must remain scheduler-ready throughout the measured interval");
|
||||
for (var witness : tickWitnesses) helper.assertTrue(level.getFluidState(witness.below()).is(net.minecraft.tags.FluidTags.WATER),
|
||||
"A real native water tick must flow downward in each forced natural-water chunk: " + witness);
|
||||
helper.assertTrue(!tickWitnesses.isEmpty(), "Fluid execution proof cannot be empty");
|
||||
report.put("fluid_execution_proof", Map.of("scope", "Independent enclosed temporary sky witnesses, excluded from natural city and water counts",
|
||||
"chunks", tickWitnesses.size(), "downward_flow_observed", true, "measured_world_ticks", level.getGameTime() - simulationStartWorldTick));
|
||||
}
|
||||
|
||||
void removeFluidWitnesses() {
|
||||
for (var witness : tickWitnesses) {
|
||||
// Empty the fluid before removing its enclosing walls; this leaves the originally empty sky unchanged.
|
||||
for (int y = 0; y >= -3; y--) level.setBlock(witness.offset(0, y, 0), Blocks.AIR.defaultBlockState(), 3);
|
||||
level.getFluidTicks().clearArea(new BoundingBox(witness.getX() - 1, witness.getY() - 4, witness.getZ() - 1,
|
||||
witness.getX() + 1, witness.getY(), witness.getZ() + 1));
|
||||
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) for (int y = -4; y <= 0; y++)
|
||||
level.setBlock(witness.offset(x, y, z), Blocks.AIR.defaultBlockState(), 3);
|
||||
}
|
||||
tickWitnesses.clear();
|
||||
}
|
||||
|
||||
String prospect() throws IOException {
|
||||
int bestScore = -1; String best = null;
|
||||
for (int candidateIndex = 0; candidateIndex < 3; candidateIndex++) {
|
||||
String id = "city15_" + round + "_" + candidateIndex;
|
||||
var candidate = session.candidate(id, "sanctuary", "east", "lost_city", 256, "natural", 2048);
|
||||
try {
|
||||
var plan = generator.lostCityPlan(candidate);
|
||||
var underground = generator.undergroundPlan(candidate);
|
||||
var kinds = new HashSet<LostCityUnderground15.Kind>();
|
||||
underground.networks().forEach(n -> n.modules().forEach(m -> kinds.add(m.kind())));
|
||||
var surfaceKinds = new HashSet<LostCityPlan.Kind>();
|
||||
plan.districts().forEach(d -> d.parcels().forEach(p -> surfaceKinds.add(p.kind())));
|
||||
long buildings = plan.districts().stream().flatMap(d -> d.parcels().stream()).filter(p -> p.kind().building()).count();
|
||||
prospects.add(Map.of("id", id, "seed", candidate.seed(), "diameter", candidate.diameter(),
|
||||
"districts", plan.districts().size(), "buildings", buildings, "underground_networks", underground.networks().size(),
|
||||
"underground_kinds", kinds.stream().map(Enum::name).sorted().toList(), "read_only", true,
|
||||
"surface_kinds", surfaceKinds.stream().map(Enum::name).sorted().toList(), "viaduct_piers", generator.citySupports(candidate).size()));
|
||||
SanctuaryMod.LOGGER.info("Lost City15 read-only prospect {} seed {}: {} districts, {} buildings, utility kinds {}",
|
||||
id, candidate.seed(), plan.districts().size(), buildings, kinds);
|
||||
report.put("passed", false); report.put("stage", "read_only_prospects"); write(level, report, false);
|
||||
int score = (int) kinds.stream().filter(REQUIRED_UNDERGROUND::contains).filter(k -> !observedKinds.contains(k)).count() * 1000
|
||||
+ (kinds.contains(LostCityUnderground15.Kind.EXPANSION_HALL) && !observedKinds.contains(LostCityUnderground15.Kind.EXPANSION_HALL) ? 150 : 0)
|
||||
+ (int) surfaceKinds.stream().filter(SURFACE_INTEREST::contains).filter(k -> !observedSurfaceKinds.contains(k)).count() * 100
|
||||
+ Math.min(20, underground.networks().size() * 4) + Math.min(10, plan.districts().size());
|
||||
if (buildings > 0 && score > bestScore) { bestScore = score; best = id; }
|
||||
} finally { generator.discardUnpublishedContext(candidate); }
|
||||
}
|
||||
round++;
|
||||
return best;
|
||||
}
|
||||
|
||||
void observeIsland(ExpansionIsland island) throws IOException {
|
||||
helper.assertTrue(island.generation() == 15, "Reference islands must retain generation 15");
|
||||
var plan = generator.lostCityPlan(island);
|
||||
var underground = generator.undergroundPlan(island);
|
||||
Map<String, Integer> surfaceTypes = new TreeMap<>(), undergroundTypes = new TreeMap<>();
|
||||
plan.districts().forEach(d -> d.parcels().forEach(p -> surfaceTypes.merge(p.kind().name(), 1, Integer::sum)));
|
||||
underground.networks().forEach(n -> n.modules().forEach(m -> undergroundTypes.merge(m.kind().name(), 1, Integer::sum)));
|
||||
naturalPlans.add(Map.of("island", island.id(), "seed", island.seed(), "diameter", island.diameter(),
|
||||
"districts", plan.districts().size(), "surface_kinds", surfaceTypes, "viaduct_supports", generator.citySupports(island).size(),
|
||||
"underground_networks", underground.networks().size(), "underground_kinds", undergroundTypes));
|
||||
SanctuaryMod.LOGGER.info("Lost City15 natural reference {} seed {} before native inspection: {} districts, surface {}, utility {}, {} piers",
|
||||
island.id(), island.seed(), plan.districts().size(), surfaceTypes, undergroundTypes, generator.citySupports(island).size());
|
||||
report.put("passed", false); report.put("stage", "native_inspection_" + island.id()); write(level, report, false);
|
||||
List<LostCityPlan.District> selected = new ArrayList<>(plan.districts());
|
||||
if (selected.isEmpty()) {
|
||||
report.put(island.origin() ? "starter_city_absent" : island.id() + "_city_absent", true);
|
||||
return;
|
||||
}
|
||||
for (int inspected = 0; inspected < MAX_DISTRICTS_PER_ISLAND && !selected.isEmpty(); inspected++) {
|
||||
selected.sort(Comparator.comparingInt((LostCityPlan.District district) -> districtScore(district, underground)).reversed()
|
||||
.thenComparingInt(LostCityPlan.District::startChunkX).thenComparingInt(LostCityPlan.District::startChunkZ));
|
||||
var district = selected.removeFirst();
|
||||
var result = observe(helper, generator, island, district, underground);
|
||||
sites.add(result);
|
||||
district.parcels().forEach(p -> {
|
||||
observedSurfaceKinds.add(p.kind());
|
||||
if (p.kind() == LostCityPlan.Kind.POOL && !expectedSurfaceWater(p).isEmpty()) wetSurfacePools.add(p);
|
||||
});
|
||||
for (var network : networks(district, underground)) for (var module : network.modules()) {
|
||||
observedKinds.add(module.kind());
|
||||
if (!expectedWater(module).isEmpty()) wetModules.add(module);
|
||||
}
|
||||
}
|
||||
}
|
||||
int districtScore(LostCityPlan.District district, LostCityUnderground15.Plan underground) {
|
||||
Set<LostCityUnderground15.Kind> kinds = new HashSet<>();
|
||||
networks(district, underground).forEach(n -> n.modules().forEach(m -> kinds.add(m.kind())));
|
||||
var surfaceKinds = district.parcels().stream().map(LostCityPlan.Parcel::kind).distinct().toList();
|
||||
return (int) kinds.stream().filter(REQUIRED_UNDERGROUND::contains).filter(k -> !observedKinds.contains(k)).count() * 1000
|
||||
+ (kinds.contains(LostCityUnderground15.Kind.EXPANSION_HALL) && !observedKinds.contains(LostCityUnderground15.Kind.EXPANSION_HALL) ? 150 : 0)
|
||||
+ (int) surfaceKinds.stream().filter(SURFACE_INTEREST::contains).filter(k -> !observedSurfaceKinds.contains(k)).count() * 300
|
||||
+ (int) surfaceKinds.stream().filter(LostCityPlan.Kind::building).filter(k -> !observedSurfaceKinds.contains(k)).count() * 20
|
||||
+ (int) district.parcels().stream().filter(p -> p.kind().building()).count();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LostCityUnderground15.Network> networks(LostCityPlan.District d, LostCityUnderground15.Plan plan) {
|
||||
return plan.networks().stream().filter(n -> n.startChunkX() == d.startChunkX() && n.startChunkZ() == d.startChunkZ()).toList();
|
||||
}
|
||||
|
||||
private static Map<String, Object> observe(GameTestHelper helper, SanctuaryChunkGenerator generator, ExpansionIsland island,
|
||||
LostCityPlan.District district, LostCityUnderground15.Plan underground) {
|
||||
var level = helper.getLevel();
|
||||
var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(LostCityStructures.KEY);
|
||||
var startChunk = level.getChunkSource().getChunk(district.startChunkX(), district.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = startChunk.getStartForStructure(holder.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted generation-15 district needs a native start");
|
||||
int modules = networks(district, underground).stream().mapToInt(n -> n.modules().size()).sum();
|
||||
long piers = generator.citySupports(island).stream().filter(s -> s.startChunkX() == district.startChunkX()
|
||||
&& s.startChunkZ() == district.startChunkZ()).count();
|
||||
helper.assertTrue(start.getPieces().size() == district.parcels().size() + modules + piers,
|
||||
"Native start must save every surface parcel, utility module and viaduct pier");
|
||||
Set<Long> chunks = new HashSet<>();
|
||||
for (var piece : start.getPieces()) piece.getBoundingBox().intersectingChunks().forEach(pos -> {
|
||||
if (!chunks.add(pos.pack())) return;
|
||||
helper.assertTrue(chunks.size() <= 225, "District inspection must stay inside the native reference footprint");
|
||||
var chunk = level.getChunkSource().getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk && chunk.getReferencesForStructure(holder.value()).contains(start.getChunkPos().pack()),
|
||||
"Every touched FULL chunk must retain the native structure reference");
|
||||
});
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var tag = start.createTag(context, start.getChunkPos());
|
||||
var loaded = StructureStart.loadStaticStart(context, tag, island.seed());
|
||||
helper.assertTrue(loaded != null && loaded.isValid() && loaded.createTag(context, loaded.getChunkPos()).equals(tag),
|
||||
"Native starts must round-trip all surface/underground fields, openings, supports and anchors");
|
||||
Map<String, Integer> surfaceKinds = new TreeMap<>(), moduleKinds = new TreeMap<>();
|
||||
List<Map<String, Object>> chests = new ArrayList<>(), rooms = new ArrayList<>(), surfaceAccess = new ArrayList<>();
|
||||
int biomes = 0, supports = 0;
|
||||
for (var piece : start.getPieces()) {
|
||||
if (piece instanceof LostCityPiece15 surface) {
|
||||
var p = surface.parcel(); surfaceKinds.merge(p.kind().name(), 1, Integer::sum);
|
||||
var centre = new BlockPos(p.minX() + p.width() / 2, p.floorY() + 1, p.minZ() + p.depth() / 2);
|
||||
if (level.getBiome(centre).is(SanctuaryMod.id("lost_city"))) biomes++;
|
||||
var monsters = generator.getMobsAt(level, level.structureManager(), MobCategory.MONSTER, centre).unwrap();
|
||||
helper.assertTrue(!monsters.isEmpty() && monsters.stream().allMatch(e -> e.value().type() == EntityTypes.ZOMBIE),
|
||||
"Native city monster overrides must contain vanilla zombies only");
|
||||
if (p.kind().building()) rooms.add(Map.of("kind", p.kind().name(), "min_x", p.minX(), "min_z", p.minZ(),
|
||||
"facing", p.facing(), "walkable_rooms", verifyInterior(helper, p, piece.getBoundingBox())));
|
||||
else if (p.kind() == LostCityPlan.Kind.BRIDGE || p.kind() == LostCityPlan.Kind.PARKING)
|
||||
surfaceAccess.add(verifySurfaceAccess(helper, p, piece.getBoundingBox()));
|
||||
scanBlocks(helper, piece.getBoundingBox(), p.kind().building() ? p.floorY() : Integer.MAX_VALUE, chests);
|
||||
} else if (piece instanceof LostCityUndergroundPiece15 utility) {
|
||||
var m = utility.module(); moduleKinds.merge(m.kind().name(), 1, Integer::sum);
|
||||
var pieceTag = utility.createTag(context);
|
||||
var decoded = new LostCityUndergroundPiece15(context, pieceTag);
|
||||
helper.assertTrue(decoded.module().equals(m) && decoded.createTag(context).equals(pieceTag), "Utility bodies and support records must persist exactly");
|
||||
supports += verifySupports(helper, m);
|
||||
scanBlocks(helper, new BoundingBox(m.minX(), m.minY(), m.minZ(), m.maxX(), m.maxY(), m.maxZ()), m.minY(), chests);
|
||||
} else if (piece instanceof LostCitySupportPiece15 pier) {
|
||||
var pieceTag = pier.createTag(context);
|
||||
var decoded = new LostCitySupportPiece15(context, pieceTag);
|
||||
helper.assertTrue(decoded.support().equals(pier.support()) && decoded.createTag(context).equals(pieceTag),
|
||||
"Viaduct pier geometry and footing must round-trip through native NBT");
|
||||
verifyPier(helper, pier); supports++;
|
||||
} else helper.assertTrue(false, "Generation 15 loaded a historical or unknown native piece type");
|
||||
}
|
||||
for (var network : networks(district, underground)) verifyNetworkAccess(helper, network);
|
||||
helper.assertTrue(chests.size() >= rooms.size(), "Every observed building needs its unopened deferred native loot chest");
|
||||
helper.assertTrue(biomes > 0, "At least one real saved city biome sample must exist per inspected district");
|
||||
var bounds = start.getBoundingBox();
|
||||
helper.assertTrue(level.getEntitiesOfClass(Villager.class, new AABB(bounds.minX(), bounds.minY(), bounds.minZ(),
|
||||
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1)).isEmpty(), "The abandoned native district must not spawn villagers");
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(holder),
|
||||
new BlockPos(district.startChunkX() * 16 + 8, 220, district.startChunkZ() * 16 + 8), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(LostCityStructures.KEY), "Exact native locate must find Lost City");
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("island", island.id()); result.put("generation", island.generation()); result.put("island_seed", island.seed());
|
||||
result.put("start_chunk", List.of(district.startChunkX(), district.startChunkZ()));
|
||||
result.put("native_start_sha256", hash(tag.toString())); result.put("native_pieces", start.getPieces().size());
|
||||
result.put("full_chunks_inspected", chunks.size()); result.put("surface_kinds", surfaceKinds); result.put("underground_kinds", moduleKinds);
|
||||
result.put("supports_checked", supports); result.put("viaduct_piers", piers);
|
||||
result.put("walkable_buildings", rooms); result.put("biome_witnesses", biomes);
|
||||
result.put("native_surface_access", surfaceAccess);
|
||||
result.put("native_city_circulation", verifyCityCirculation(helper, district.parcels()));
|
||||
result.put("unopened_chests", chests); result.put("native_zombie_only_monster_spawns", true);
|
||||
result.put("native_locate", coords(located.getFirst()));
|
||||
district.parcels().stream().filter(p -> p.kind().building()).findFirst().ifPresent(first ->
|
||||
result.put("edit_position", List.of(first.minX(), first.floorY() + 1, first.minZ())));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void scanBlocks(GameTestHelper helper, BoundingBox box, int noOreFromY, List<Map<String, Object>> chests) {
|
||||
var level = helper.getLevel();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
for (int y = box.minY(); y <= box.maxY(); y++) {
|
||||
var pos = new BlockPos(x, y, z); var block = level.getBlockState(pos);
|
||||
helper.assertTrue(!block.is(Blocks.SPAWNER), "Native city must not contain a zombie spawner");
|
||||
if (y >= noOreFromY) helper.assertTrue(!BuiltInRegistries.BLOCK.getKey(block.getBlock()).getPath().endsWith("_ore"),
|
||||
"Native ore decoration replaced protected masonry/interiors at " + pos);
|
||||
if (block.is(Blocks.CHEST) && level.getBlockEntity(pos) instanceof RandomizableContainerBlockEntity chest && chest.getLootTable() != null)
|
||||
chests.add(Map.of("position", coords(pos), "table", chest.getLootTable().identifier().toString(), "seed", chest.getLootTableSeed()));
|
||||
}
|
||||
}
|
||||
|
||||
private record StreetSection(String description, List<BlockPos> candidates) { }
|
||||
|
||||
/** Only drawn roads and the two front porch columns are walkable in this proof. Natural terrain,
|
||||
* building interiors and roofs cannot disguise disconnected streets or an isolated raised lot. */
|
||||
private static Map<String, Object> verifyCityCirculation(GameTestHelper helper, List<LostCityPlan.Parcel> parcels) {
|
||||
Set<BlockPos> permitted = new HashSet<>();
|
||||
List<StreetSection> sections = new ArrayList<>();
|
||||
List<Map<String, Object>> entrances = new ArrayList<>();
|
||||
List<BlockPos> targets = new ArrayList<>();
|
||||
int streets = 0;
|
||||
for (var parcel : parcels) {
|
||||
if (parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE) {
|
||||
streets++;
|
||||
for (int x = 0; x < parcel.width(); x++) for (int z = 0; z < parcel.depth(); z++)
|
||||
for (int rise = 1; rise <= 2; rise++) permitted.add(new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + rise, parcel.minZ() + z));
|
||||
boolean eastWest = parcel.width() >= parcel.depth();
|
||||
int length = eastWest ? parcel.width() : parcel.depth();
|
||||
int breadth = eastWest ? parcel.depth() : parcel.width();
|
||||
for (int along = 0; along < length; along++) {
|
||||
List<BlockPos> options = new ArrayList<>();
|
||||
// A closed utility hatch may occupy the exact middle; the adjacent traffic lane
|
||||
// must still cross this complete street section without leaving the road.
|
||||
for (int across = Math.max(0, breadth / 2 - 1); across <= Math.min(breadth - 1, breadth / 2 + 1); across++)
|
||||
for (int rise = 1; rise <= 2; rise++) {
|
||||
int x = eastWest ? along : across, z = eastWest ? across : along;
|
||||
options.add(new BlockPos(parcel.minX() + x, parcel.surfaceY(x, z) + rise, parcel.minZ() + z));
|
||||
}
|
||||
sections.add(new StreetSection(parcel.kind() + " at " + parcel.minX() + "," + parcel.minZ() + " section " + along, options));
|
||||
}
|
||||
} else {
|
||||
BlockPos entrance = rotatedWorld(parcel, parcel.width() / 2, 1, 0);
|
||||
targets.add(entrance);
|
||||
entrances.add(Map.of("kind", parcel.kind().name(), "position", coords(entrance)));
|
||||
permitted.add(entrance);
|
||||
permitted.add(rotatedWorld(parcel, parcel.width() / 2 - 1, 1, 0));
|
||||
}
|
||||
}
|
||||
helper.assertTrue(streets > 0 && !targets.isEmpty(), "A native city must connect actual lots to drawn streets");
|
||||
var source = targets.getFirst();
|
||||
var reached = walk(helper, source, permitted::contains);
|
||||
for (var target : targets) helper.assertTrue(reached.contains(target),
|
||||
"Native city entrance is isolated from its road network: " + source + " to " + target);
|
||||
List<List<Integer>> witnessedSections = new ArrayList<>();
|
||||
for (var section : sections) {
|
||||
var witness = section.candidates().stream().filter(reached::contains).findFirst();
|
||||
helper.assertTrue(witness.isPresent(), "Native street is blocked or disconnected: " + section.description()
|
||||
+ "; candidates " + section.candidates());
|
||||
witnessedSections.add(coords(witness.orElseThrow()));
|
||||
}
|
||||
SanctuaryMod.LOGGER.info("Lost City15 native circulation: {} lots, {} street/bridge pieces, {} crossed sections, {} standing cells from {}",
|
||||
targets.size(), streets, sections.size(), reached.size(), source);
|
||||
return Map.of("scope", "Connected drawn streets and front porches only; no natural terrain, interior or roof shortcuts",
|
||||
"source", coords(source), "entrances", entrances, "street_pieces", streets,
|
||||
"crossed_street_sections", witnessedSections, "reachable_standing_cells", reached.size(), "connected", true);
|
||||
}
|
||||
|
||||
private static int verifyInterior(GameTestHelper helper, LostCityPlan.Parcel p, BoundingBox bounds) {
|
||||
var level = helper.getLevel();
|
||||
Map<BlockPos, BlockState> doors = openDoors(helper, bounds, false);
|
||||
try {
|
||||
var targets = LostCityInteriors15.rooms(p);
|
||||
Set<BlockPos> reached = walk(helper, world(p, targets.getFirst()), bounds::isInside);
|
||||
for (var room : targets) helper.assertTrue(reached.contains(world(p, room)),
|
||||
"Native " + p.kind() + " cannot reach room " + room.name() + " at " + world(p, room));
|
||||
helper.assertTrue(!doors.isEmpty(), "Detailed interiors need real two-block doors");
|
||||
for (var entry : doors.entrySet()) if (entry.getValue().getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER) {
|
||||
Direction normal = entry.getValue().getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
helper.assertTrue(reached.contains(entry.getKey().relative(normal)) && reached.contains(entry.getKey().relative(normal.getOpposite())),
|
||||
"A placed native door has an inaccessible side at " + entry.getKey());
|
||||
for (Direction side : new Direction[]{normal.getClockWise(), normal.getCounterClockWise()}) {
|
||||
var neighbor = doors.get(entry.getKey().relative(side));
|
||||
if (neighbor == null || neighbor.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) != DoubleBlockHalf.LOWER
|
||||
|| neighbor.getValue(BlockStateProperties.HORIZONTAL_FACING) != normal) continue;
|
||||
var opened = level.getBlockState(entry.getKey()).getCollisionShape(level, entry.getKey()).bounds();
|
||||
helper.assertTrue(switch (side) {
|
||||
case EAST -> opened.maxX <= .25; case WEST -> opened.minX >= .75;
|
||||
case SOUTH -> opened.maxZ <= .25; case NORTH -> opened.minZ >= .75;
|
||||
default -> false;
|
||||
}, "Native double door opens into its partner instead of against its outer jamb at " + entry.getKey());
|
||||
}
|
||||
}
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x++) for (int z = bounds.minZ(); z <= bounds.maxZ(); z++)
|
||||
for (int y = p.floorY() + 1; y <= p.maxY(); y++) {
|
||||
var at = new BlockPos(x, y, z); var state = level.getBlockState(at);
|
||||
if (state.is(Blocks.CHEST)) helper.assertTrue(adjacent(at, reached), "Native loot chest cannot be approached from its room");
|
||||
if (state.is(Blocks.LANTERN)) {
|
||||
var support = state.getValue(BlockStateProperties.HANGING) ? at.above() : at.below();
|
||||
helper.assertTrue(level.getBlockState(support).isCollisionShapeFullBlock(level, support) && state.canSurvive(level, at),
|
||||
"Native lantern floats against a slab or lacks a complete support at " + at);
|
||||
}
|
||||
if (state.is(Blocks.DARK_OAK_FENCE) && level.getBlockState(at.above()).is(Blocks.DARK_OAK_PRESSURE_PLATE)) {
|
||||
for (var side : HORIZONTAL) {
|
||||
var chair = at.relative(side); var seat = level.getBlockState(chair);
|
||||
if (!seat.is(Blocks.DARK_OAK_STAIRS)) continue;
|
||||
var shape = seat.getCollisionShape(level, chair).toAabbs();
|
||||
double nearX = .5 - side.getStepX() * .25, nearZ = .5 - side.getStepZ() * .25;
|
||||
double farX = .5 + side.getStepX() * .25, farZ = .5 + side.getStepZ() * .25;
|
||||
helper.assertTrue(shape.stream().noneMatch(box -> box.contains(nearX, .75, nearZ))
|
||||
&& shape.stream().anyMatch(box -> box.contains(farX, .75, farZ)),
|
||||
"Native seat has its back toward its table or desk at " + chair);
|
||||
}
|
||||
}
|
||||
if (!state.is(Blocks.BED.white())) continue;
|
||||
var part = state.getValue(BlockStateProperties.BED_PART);
|
||||
var facing = state.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
var partner = at.relative(part == BedPart.FOOT ? facing : facing.getOpposite());
|
||||
helper.assertTrue(level.getBlockState(partner).equals(state.setValue(BlockStateProperties.BED_PART,
|
||||
part == BedPart.FOOT ? BedPart.HEAD : BedPart.FOOT)), "Native placement severed a bed at " + at);
|
||||
helper.assertTrue(adjacent(at, reached) || adjacent(partner, reached), "Native bed cannot be approached");
|
||||
}
|
||||
return targets.size();
|
||||
} finally { doors.forEach((pos, state) -> level.setBlock(pos, state, 2)); }
|
||||
}
|
||||
|
||||
private static Map<BlockPos, BlockState> openDoors(GameTestHelper helper, BoundingBox bounds, boolean hatch) {
|
||||
var level = helper.getLevel(); Map<BlockPos, BlockState> originals = new HashMap<>();
|
||||
for (int x = bounds.minX(); x <= bounds.maxX(); x++) for (int z = bounds.minZ(); z <= bounds.maxZ(); z++)
|
||||
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
|
||||
var pos = new BlockPos(x, y, z); var state = level.getBlockState(pos);
|
||||
if (state.getBlock() instanceof DoorBlock) {
|
||||
helper.assertTrue(state.is(Blocks.DARK_OAK_DOOR), "Room doors must be manually openable");
|
||||
boolean lower = state.getValue(BlockStateProperties.DOUBLE_BLOCK_HALF) == DoubleBlockHalf.LOWER;
|
||||
helper.assertTrue(level.getBlockState(lower ? pos.above() : pos.below()).equals(state.setValue(
|
||||
BlockStateProperties.DOUBLE_BLOCK_HALF, lower ? DoubleBlockHalf.UPPER : DoubleBlockHalf.LOWER)),
|
||||
"Native door halves disagree at " + pos);
|
||||
originals.put(pos, state);
|
||||
} else if (hatch && state.is(Blocks.DARK_OAK_TRAPDOOR)) originals.put(pos, state);
|
||||
}
|
||||
// The test opens both native halves, measures the route, then restores all original states.
|
||||
originals.forEach((pos, state) -> level.setBlock(pos, state.setValue(BlockStateProperties.OPEN, true), 2));
|
||||
return originals;
|
||||
}
|
||||
|
||||
private static void verifyNetworkAccess(GameTestHelper helper, LostCityUnderground15.Network network) {
|
||||
var access = network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS).findFirst().orElseThrow();
|
||||
var bounds = LostCityUndergroundPiece15.bounds(access);
|
||||
var doors = openDoors(helper, bounds, true);
|
||||
try {
|
||||
BlockPos hatch = doors.keySet().stream().filter(pos -> doors.get(pos).is(Blocks.DARK_OAK_TRAPDOOR)).findFirst().orElseThrow();
|
||||
Predicate<BlockPos> inside = pos -> network.modules().stream().anyMatch(m -> m.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
BlockPos approach = java.util.Arrays.stream(HORIZONTAL).map(hatch::relative)
|
||||
.filter(inside).filter(pos -> standable(helper.getLevel(), pos)).findFirst().orElseThrow();
|
||||
Set<BlockPos> reached = walk(helper, approach, inside);
|
||||
helper.assertTrue(reached.contains(hatch), "Surface hatch must be accessible from its street-level approach");
|
||||
for (var m : network.modules()) helper.assertTrue(reached.stream().anyMatch(pos -> pos.getY() == m.floorY() + 1
|
||||
&& pos.getX() > m.minX() && pos.getX() < m.maxX() && pos.getZ() > m.minZ() && pos.getZ() < m.maxZ()),
|
||||
"Native surface hatch cannot reach utility module " + m.kind() + " at " + m.centerX() + "," + m.centerZ());
|
||||
for (var m : network.modules()) if (m.kind() == LostCityUnderground15.Kind.EXPANSION_HALL) {
|
||||
helper.assertTrue(reached.contains(new BlockPos(m.centerX(), m.minY() + 3, m.centerZ())),
|
||||
"Native expansion hall's reserved central plinth cannot be reached from the surface hatch");
|
||||
for (int dx : new int[]{-6, 6}) for (int dz : new int[]{-6, 6})
|
||||
helper.assertTrue(reached.contains(new BlockPos(m.centerX() + dx, m.floorY() + 1, m.centerZ() + dz)),
|
||||
"Native expansion hall has an inaccessible quarter at " + m.centerX() + "," + m.centerZ());
|
||||
}
|
||||
} finally { doors.forEach((pos, state) -> helper.getLevel().setBlock(pos, state, 2)); }
|
||||
}
|
||||
|
||||
private static int verifySupports(GameTestHelper helper, LostCityUnderground15.Module m) {
|
||||
var level = helper.getLevel();
|
||||
for (var support : m.supports()) {
|
||||
helper.assertTrue(support.maxY() - support.minY() < 8, "Native support exceeds its bounded rock anchor distance");
|
||||
for (int y = support.minY(); y <= support.maxY(); y++) {
|
||||
var expected = support.type() == LostCityUnderground15.Type.PILLAR ? Blocks.STONE_BRICKS
|
||||
: y == support.maxY() ? Blocks.POLISHED_ANDESITE : Blocks.IRON_CHAIN;
|
||||
helper.assertTrue(level.getBlockState(new BlockPos(support.x(), y, support.z())).is(expected),
|
||||
"Native supported module has a broken or overwritten pillar/chain");
|
||||
}
|
||||
int neighborY = support.type() == LostCityUnderground15.Type.PILLAR ? support.minY() - 1 : support.maxY() + 1;
|
||||
var neighbor = level.getBlockState(new BlockPos(support.x(), neighborY, support.z()));
|
||||
helper.assertTrue(neighbor.isSolid() && neighbor.getFluidState().isEmpty(), "Native support lost its rock anchor");
|
||||
}
|
||||
return m.supports().size();
|
||||
}
|
||||
|
||||
private static void verifyPier(GameTestHelper helper, LostCitySupportPiece15 piece) {
|
||||
var support = piece.support(); var level = helper.getLevel();
|
||||
helper.assertTrue(support.maxY() - support.minY() < 16, "Native viaduct grew an unbounded pier");
|
||||
for (int x = support.minX(); x <= support.maxX(); x++) for (int z = support.minZ(); z <= support.maxZ(); z++) {
|
||||
var anchor = level.getBlockState(new BlockPos(x, support.minY() - 1, z));
|
||||
helper.assertTrue(anchor.isSolid() && anchor.getFluidState().isEmpty(), "Native viaduct pier lost its natural footing");
|
||||
for (int y = support.minY(); y <= support.maxY(); y++) {
|
||||
var expected = y == support.minY() || y == support.maxY() ? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||
: LostCityPalette.masonry(support.seed(), x - support.minX(), y - support.minY(), z - support.minZ());
|
||||
helper.assertTrue(level.getBlockState(new BlockPos(x, y, z)).equals(expected), "Native viaduct pier is interrupted or overwritten");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> verifySurfaceAccess(GameTestHelper helper, LostCityPlan.Parcel p, BoundingBox bounds) {
|
||||
BlockPos start, target;
|
||||
if (p.kind() == LostCityPlan.Kind.BRIDGE) {
|
||||
boolean eastWest = p.width() >= p.depth();
|
||||
int startX = eastWest ? 0 : p.width() / 2, startZ = eastWest ? p.depth() / 2 : 0;
|
||||
int endX = eastWest ? p.width() - 1 : p.width() / 2, endZ = eastWest ? p.depth() / 2 : p.depth() - 1;
|
||||
start = new BlockPos(p.minX() + startX, p.surfaceY(startX, startZ) + 1, p.minZ() + startZ);
|
||||
target = new BlockPos(p.minX() + endX, p.surfaceY(endX, endZ) + 1, p.minZ() + endZ);
|
||||
if (!standable(helper.getLevel(), start) && helper.getLevel().getBlockState(start).is(Blocks.STONE_BRICK_STAIRS)) start = start.above();
|
||||
if (!standable(helper.getLevel(), target) && helper.getLevel().getBlockState(target).is(Blocks.STONE_BRICK_STAIRS)) target = target.above();
|
||||
} else {
|
||||
start = rotatedWorld(p, 8, 1, 0);
|
||||
target = rotatedWorld(p, 8, p.foundationDepth() >= 8 ? -4 : 1, 10);
|
||||
}
|
||||
Set<BlockPos> reached = walk(helper, start, bounds::isInside);
|
||||
helper.assertTrue(reached.contains(target), "Native " + p.kind() + " lost its traversable access: " + start + " to " + target);
|
||||
return Map.of("kind", p.kind().name(), "start", coords(start), "destination", coords(target),
|
||||
"underground_garage", p.kind() == LostCityPlan.Kind.PARKING && p.foundationDepth() >= 8, "walkable", true);
|
||||
}
|
||||
|
||||
private static Set<BlockPos> expectedSurfaceWater(LostCityPlan.Parcel p) {
|
||||
return surfaceWater(p).sources();
|
||||
}
|
||||
|
||||
private record SurfaceWater(Set<BlockPos> sources, Set<BlockPos> flowEnvelope,
|
||||
Map<BlockPos, BlockState> shell, Map<BlockPos, BlockState> steps) { }
|
||||
|
||||
private static SurfaceWater surfaceWater(LostCityPlan.Parcel p) {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
LostCitySurface15.render(p, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
blocks.put(new BlockPos(p.minX() + x, p.floorY() + y, p.minZ() + z), state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { }
|
||||
});
|
||||
Set<BlockPos> sources = new HashSet<>(), basin = new HashSet<>();
|
||||
Map<BlockPos, BlockState> shell = new HashMap<>(), steps = new HashMap<>();
|
||||
blocks.forEach((pos, state) -> { if (!state.getFluidState().isEmpty()) sources.add(pos); });
|
||||
for (int x = 4; x <= p.width() - 5; x++) for (int z = 5; z <= p.depth() - 5; z++)
|
||||
for (int y = -3; y <= -1; y++) basin.add(rotatedWorld(p, x, y, z));
|
||||
// The submerged steps deliberately clear cells above their lower treads. Native water
|
||||
// may fill those cells; this finite closure permits that flow without permitting a leak.
|
||||
Set<BlockPos> envelope = new HashSet<>(sources);
|
||||
ArrayDeque<BlockPos> queue = new ArrayDeque<>(sources);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
if (!basin.contains(pos)) throw new IllegalStateException("Rendered pool has an open water boundary at " + pos);
|
||||
for (var direction : Direction.values()) if (direction != Direction.UP) {
|
||||
var next = pos.relative(direction);
|
||||
var state = blocks.getOrDefault(next, Blocks.AIR.defaultBlockState());
|
||||
if (state.isAir() || !state.getFluidState().isEmpty()) {
|
||||
if (envelope.add(next)) queue.addLast(next);
|
||||
} else shell.put(next, state);
|
||||
}
|
||||
}
|
||||
for (int step = 1; step <= 3; step++) for (int x = p.width() / 2 - 1; x <= p.width() / 2 + 1; x++) {
|
||||
var pos = rotatedWorld(p, x, -step, step + 4);
|
||||
if (!sources.isEmpty()) steps.put(pos, blocks.get(pos));
|
||||
}
|
||||
return new SurfaceWater(Set.copyOf(sources), Set.copyOf(envelope), Map.copyOf(shell), Map.copyOf(steps));
|
||||
}
|
||||
|
||||
private static Map<String, Object> verifySurfacePool(GameTestHelper helper, LostCityPlan.Parcel p, boolean ticked) {
|
||||
var level = helper.getLevel(); var water = surfaceWater(p);
|
||||
helper.assertTrue(water.sources().size() >= 150, "Naturally planned surface pool must contain a substantial basin");
|
||||
var witness = water.sources().stream().sorted(BlockPos::compareTo).findFirst().orElseThrow();
|
||||
if (ticked) {
|
||||
helper.assertTrue(level.isPositionTickingWithEntitiesLoaded(ChunkPos.containing(witness).pack()), "Natural surface pool must actually tick");
|
||||
}
|
||||
int count = 0;
|
||||
for (int x = p.minX(); x <= p.maxX(); x++) for (int z = p.minZ(); z <= p.maxZ(); z++)
|
||||
for (int y = p.minY(); y <= p.maxY(); y++) {
|
||||
var at = new BlockPos(x, y, z); boolean wet = !level.getFluidState(at).isEmpty();
|
||||
helper.assertTrue(!wet || water.flowEnvelope().contains(at), "Native surface pool leaked outside its sealed basin at " + at);
|
||||
helper.assertTrue(!wet || level.getFluidState(at).is(net.minecraft.tags.FluidTags.WATER), "Native surface pool contains a foreign fluid at " + at);
|
||||
helper.assertTrue(!water.sources().contains(at) || wet && level.getFluidState(at).isSource(),
|
||||
"Native surface pool lost an original water source at " + at);
|
||||
if (wet) count++;
|
||||
}
|
||||
water.shell().forEach((pos, expected) -> helper.assertTrue(level.getBlockState(pos).equals(expected),
|
||||
"Native surface pool lost its continuous floor or walls at " + pos));
|
||||
water.steps().forEach((pos, expected) -> helper.assertTrue(level.getBlockState(pos).equals(expected),
|
||||
"Native pool steps changed or lost their waterlogging after fluid ticks/reload at " + pos));
|
||||
return Map.of("scope", "Naturally planned surface pool, not an isolated fixture", "min", List.of(p.minX(), p.floorY(), p.minZ()),
|
||||
"facing", p.facing(), "retained_water_cells", count, "checked_after_observed_fluid_simulation", ticked,
|
||||
"contained", true, "steps_preserved", true, "original_water_cells", water.sources().size(),
|
||||
"flow_reachable_cells", water.flowEnvelope().size(), "new_intra_basin_water_cells", count - water.sources().size());
|
||||
}
|
||||
|
||||
private static Set<BlockPos> expectedWater(LostCityUnderground15.Module module) {
|
||||
Set<BlockPos> water = new HashSet<>();
|
||||
LostCityUnderground15.render(module, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
var pos = new BlockPos(module.minX() + x, module.minY() + y, module.minZ() + z);
|
||||
if (!state.getFluidState().isEmpty()) water.add(pos); else water.remove(pos);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) { }
|
||||
});
|
||||
return water;
|
||||
}
|
||||
|
||||
private static Map<String, Object> verifyWater(GameTestHelper helper, LostCityUnderground15.Module m, boolean ticked) {
|
||||
var level = helper.getLevel(); var water = expectedWater(m);
|
||||
BlockPos witness = water.stream().sorted(BlockPos::compareTo).findFirst().orElseThrow();
|
||||
if (ticked) {
|
||||
helper.assertTrue(level.isPositionTickingWithEntitiesLoaded(ChunkPos.containing(witness).pack()), "Natural poolroom/sewer fixture must actually tick");
|
||||
}
|
||||
int count = 0;
|
||||
for (int x = m.minX(); x <= m.maxX(); x++) for (int z = m.minZ(); z <= m.maxZ(); z++)
|
||||
for (int y = m.minY(); y <= m.maxY(); y++) {
|
||||
var pos = new BlockPos(x, y, z); boolean wet = !level.getFluidState(pos).isEmpty();
|
||||
helper.assertTrue(wet == water.contains(pos), "Natural utility water leaked or disappeared at " + pos + " in " + m.kind());
|
||||
if (wet) count++;
|
||||
}
|
||||
helper.assertTrue(count > 0, "Native wet module needs retained water");
|
||||
return Map.of("scope", "Naturally planned and natively generated utility module", "kind", m.kind().name(),
|
||||
"min", List.of(m.minX(), m.minY(), m.minZ()), "retained_water_cells", count,
|
||||
"checked_after_observed_fluid_simulation", ticked, "contained", true);
|
||||
}
|
||||
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel();
|
||||
helper.assertTrue(inside.test(start) && standable(level, start), "Native route has a blocked starting position: " + start);
|
||||
Set<BlockPos> reached = new HashSet<>(); ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||
reached.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (Direction d : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(d).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !passable(level.getBlockState(pos.above(2)))) continue;
|
||||
if (standable(level, next) && reached.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[]{-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0);
|
||||
if (!inside.test(next)) continue;
|
||||
if ((climbable(level.getBlockState(pos)) || climbable(level.getBlockState(next)) || climbable(level.getBlockState(pos.below())))
|
||||
&& standable(level, next) && reached.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(reached.size() < 100_000, "Native room traversal exceeded its bounded structural footprint");
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
var below = level.getBlockState(pos.below());
|
||||
return passable(level.getBlockState(pos)) && passable(level.getBlockState(pos.above()))
|
||||
&& (below.isSolid() || below.getBlock() instanceof StairBlock || climbable(level.getBlockState(pos)) || climbable(below));
|
||||
}
|
||||
private static boolean climbable(BlockState state) { return state.is(Blocks.LADDER); }
|
||||
private static boolean passable(BlockState state) {
|
||||
return state.isAir() || state.is(Blocks.LADDER) || state.is(Blocks.REDSTONE_WALL_TORCH)
|
||||
|| (state.is(Blocks.DARK_OAK_DOOR) || state.is(Blocks.DARK_OAK_TRAPDOOR)) && state.getValue(BlockStateProperties.OPEN);
|
||||
}
|
||||
private static boolean adjacent(BlockPos pos, Set<BlockPos> reached) {
|
||||
for (Direction direction : HORIZONTAL) if (reached.contains(pos.relative(direction))) return true;
|
||||
return false;
|
||||
}
|
||||
private static BlockPos world(LostCityPlan.Parcel p, LostCityInteriors15.Room room) {
|
||||
return new BlockPos(p.minX() + room.x(), p.floorY() + room.y(), p.minZ() + room.z());
|
||||
}
|
||||
private static BlockPos rotatedWorld(LostCityPlan.Parcel p, int x, int y, int z) {
|
||||
int rx = switch (p.facing()) { case 0 -> x; case 1 -> p.width() - 1 - z; case 2 -> p.width() - 1 - x; default -> z; };
|
||||
int rz = switch (p.facing()) { case 0 -> z; case 1 -> x; case 2 -> p.depth() - 1 - z; default -> p.depth() - 1 - x; };
|
||||
return new BlockPos(p.minX() + rx, p.floorY() + y, p.minZ() + rz);
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> verifyInteriorModels(GameTestHelper helper) {
|
||||
var context = StructurePieceSerializationContext.fromLevel(helper.getLevel());
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (var kind : LostCityPlan.Kind.values()) if (kind.building()) for (int facing = 0; facing < 4; facing++) {
|
||||
var parcel = new LostCityPlan.Parcel(kind, -32, 48, 17, 17, 160, 3, facing, 1515, 4, new int[0]);
|
||||
var piece = new LostCityPiece15(parcel); var tag = piece.createTag(context);
|
||||
var reloaded = new LostCityPiece15(context, tag);
|
||||
helper.assertTrue(reloaded.createTag(context).equals(tag), "Generation-15 interior parcel fields did not persist");
|
||||
helper.assertTrue(LostCityInteriors15.rooms(parcel).equals(LostCityInteriors15.rooms(reloaded.parcel())), "Reload changed room targets");
|
||||
result.add(Map.of("kind", kind.name(), "facing", facing, "native_piece_sha256", hash(tag.toString()),
|
||||
"scope", "Serialization fixture only; natural occurrence is recorded separately in sites"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void verifyLegacy(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, helper.getLevel().registryAccess());
|
||||
for (int generation : new int[]{13, 14}) {
|
||||
var json = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
json.addProperty("type", "sanctuary:island_v" + generation);
|
||||
json.getAsJsonObject("biome_source").addProperty("type", "sanctuary:island_v" + generation);
|
||||
var legacy = (SanctuaryChunkGenerator) ChunkGenerator.CODEC.parse(ops, json).getOrThrow();
|
||||
helper.assertTrue(legacy.rootGeneration() == generation && legacy.journalDirectory().equals("sanctuary-world-v" + generation),
|
||||
"Historical codecs must retain their exact generation and journal");
|
||||
helper.assertTrue(legacy.undergroundPlan(ExpansionIsland.origin(generator.initialDiameter(), helper.getLevel().getSeed(), generation)).isEmpty(),
|
||||
"Historical worlds must not acquire generation-15 underground utilities");
|
||||
}
|
||||
}
|
||||
|
||||
private static void reload(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
var level = helper.getLevel(); var result = report(level);
|
||||
try {
|
||||
var previous = JsonParser.parseString(Files.readString(path(level, false))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload requires the successful creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a second JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed() && previous.get("world_directory").getAsString().equals(
|
||||
level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString()), "Reload must use the same development world and seed");
|
||||
var session = ExpansionRuntime.session(level);
|
||||
helper.assertTrue(previous.get("islands").equals(ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, session.islands()).getOrThrow()), "Island identities changed on reload");
|
||||
helper.assertTrue(hash(Files.readString(journal(level, generator))).equals(previous.get("journal_sha256").getAsString()), "Expansion journal bytes changed on reload");
|
||||
var gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Existing gameplay edit was lost or regenerated");
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(LostCityStructures.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
int savedPieces = 0, unopened = 0, roomTargets = 0, supports = 0;
|
||||
List<Map<String, Object>> fluids = new ArrayList<>(), surfacePools = new ArrayList<>(), surfaceAccess = new ArrayList<>(), circulation = new ArrayList<>();
|
||||
Set<String> checkedNetworks = new HashSet<>();
|
||||
for (var element : previous.getAsJsonArray("sites")) {
|
||||
var site = element.getAsJsonObject(); var pos = site.getAsJsonArray("start_chunk");
|
||||
var chunk = level.getChunk(pos.get(0).getAsInt(), pos.get(1).getAsInt(), ChunkStatus.FULL, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Saved generation-15 native start was lost");
|
||||
helper.assertTrue(hash(start.createTag(context, start.getChunkPos()).toString()).equals(site.get("native_start_sha256").getAsString()),
|
||||
"Native pieces, utility bodies, openings or support anchors changed on reload");
|
||||
savedPieces += start.getPieces().size();
|
||||
for (var piece : start.getPieces()) {
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(start.getChunkPos().pack()), "Saved FULL reference disappeared");
|
||||
});
|
||||
if (piece instanceof LostCityPiece15 surface) {
|
||||
var p = surface.parcel();
|
||||
if (p.kind().building()) roomTargets += verifyInterior(helper, p, piece.getBoundingBox());
|
||||
else if (p.kind() == LostCityPlan.Kind.POOL && !expectedSurfaceWater(p).isEmpty()) surfacePools.add(verifySurfacePool(helper, p, false));
|
||||
else if (p.kind() == LostCityPlan.Kind.BRIDGE || p.kind() == LostCityPlan.Kind.PARKING)
|
||||
surfaceAccess.add(verifySurfaceAccess(helper, p, piece.getBoundingBox()));
|
||||
} else if (piece instanceof LostCityUndergroundPiece15 utility) {
|
||||
supports += verifySupports(helper, utility.module());
|
||||
if (!expectedWater(utility.module()).isEmpty()) fluids.add(verifyWater(helper, utility.module(), false));
|
||||
} else if (piece instanceof LostCitySupportPiece15 pier) { verifyPier(helper, pier); supports++; }
|
||||
}
|
||||
var savedParcels = start.getPieces().stream().filter(LostCityPiece15.class::isInstance)
|
||||
.map(LostCityPiece15.class::cast).map(LostCityPiece15::parcel).toList();
|
||||
circulation.add(verifyCityCirculation(helper, savedParcels));
|
||||
for (var chestElement : site.getAsJsonArray("unopened_chests")) {
|
||||
var saved = chestElement.getAsJsonObject(); var at = readPos(saved.getAsJsonArray("position"));
|
||||
helper.assertTrue(level.getBlockEntity(at) instanceof RandomizableContainerBlockEntity, "Saved deferred chest entity disappeared");
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(at);
|
||||
helper.assertTrue(chest.getLootTable() != null && chest.getLootTable().identifier().toString().equals(saved.get("table").getAsString())
|
||||
&& chest.getLootTableSeed() == saved.get("seed").getAsLong(), "Unopened loot table or seed changed on reload");
|
||||
unopened++;
|
||||
}
|
||||
var island = session.island(site.get("island").getAsString());
|
||||
for (var network : generator.undergroundPlan(island).networks()) {
|
||||
String identity = island.id() + "/" + network.startChunkX() + "/" + network.startChunkZ();
|
||||
if (network.startChunkX() == pos.get(0).getAsInt() && network.startChunkZ() == pos.get(1).getAsInt() && checkedNetworks.add(identity))
|
||||
verifyNetworkAccess(helper, network);
|
||||
}
|
||||
}
|
||||
var readback = new ExpansionJournal(journal(level, generator).getParent(), level.getSeed(), generator.initialDiameter(), 15);
|
||||
helper.assertTrue(readback.healthy() && readback.islands().equals(session.islands()), "Generation-15 journal failed its own strict reopen");
|
||||
result.put("saved_native_pieces", savedPieces); result.put("preserved_deferred_chests", unopened);
|
||||
result.put("walkable_room_targets", roomTargets); result.put("preserved_supports", supports);
|
||||
result.put("natural_surface_pool_checks", surfacePools); result.put("native_surface_access", surfaceAccess);
|
||||
result.put("native_city_circulation", circulation);
|
||||
result.put("natural_fluid_checks", fluids); result.put("gold_witness_preserved", true); result.put("journal_preserved", true);
|
||||
result.put("passed", true); write(level, result, true); helper.succeed();
|
||||
} catch (Exception error) {
|
||||
result.put("passed", false); result.put("failure", error.toString());
|
||||
try { write(level, result, true); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Lost City15 native reload failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> report(ServerLevel level) {
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("seed", level.getSeed()); result.put("process_id", ProcessHandle.current().pid());
|
||||
result.put("world_directory", level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString());
|
||||
result.put("scope", "Naturally planned generation-15 districts and utility networks in a disposable development world. "
|
||||
+ "At most six read-only prospects and two 256-block expansions; inspection loads at most six district footprints per island, not an entire island. "
|
||||
+ "Normal expansion preparation retains its own bounded chunk scope. Interior serialization fixtures are not natural occurrence evidence. "
|
||||
+ "Independent enclosed temporary sky witnesses prove native fluid execution in each forced chunk and are removed before saving; they are excluded from natural water counts. "
|
||||
+ "Native doors/hatches are temporarily opened and restored for player-sized route checks. No personal world is opened.");
|
||||
return result;
|
||||
}
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path journal(ServerLevel level, SanctuaryChunkGenerator generator) {
|
||||
return level.getServer().getWorldPath(LevelResource.ROOT).resolve("data").resolve(generator.journalDirectory()).resolve("expansions.json");
|
||||
}
|
||||
private static Path path(ServerLevel level, boolean reload) {
|
||||
return Path.of("diagnostics", "lost-city-alpha15" + (reload ? "-reload" : "") + "-seed-" + level.getSeed() + ".json");
|
||||
}
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = path(level, reload); Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
}
|
||||
private static String hash(String text) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(text.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryDecorationProtection;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlanner16;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityProtection16;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityTerrain16;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Six isolated, real starter-noise witnesses. This helper never requests a chunk, attaches a
|
||||
* generator, mutates the active biome source or creates an expansion session. Hydrology is the
|
||||
* production planner and its exact protection mask, rather than an all-dry testing substitute.
|
||||
* Surface availability is asserted; underground coverage is reported as a survey, not a guarantee. */
|
||||
public final class LostCity16PlanningDiagnostics {
|
||||
private LostCity16PlanningDiagnostics() {}
|
||||
|
||||
public static List<Map<String, Object>> verify(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
helper.assertTrue(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"Native city matrix requires the Sanctuary source");
|
||||
var active = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(active.rootGeneration() == 16 && level.getMinY() == 0 && level.getHeight() == 384,
|
||||
"Native city matrix uses generation16 and its actual 384-block height");
|
||||
var originalIslands = active.source().islands();
|
||||
var access = level.registryAccess();
|
||||
var settings = access.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
var noises = access.lookupOrThrow(Registries.NOISE);
|
||||
var reports = new ArrayList<Map<String, Object>>();
|
||||
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1}) {
|
||||
long started = System.nanoTime();
|
||||
int capacity = diameter == 512 ? 5 : diameter == 724 ? 10 : 20;
|
||||
var key = ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_" + capacity));
|
||||
var registered = settings.getOrThrow(key);
|
||||
var island = ExpansionIsland.origin(diameter, seed, 16);
|
||||
var source = active.source().copy();
|
||||
helper.assertTrue(source != active.source() && source.generation() == 16, "Matrix shares a mutable biome source");
|
||||
source.setIslands(List.of(island));
|
||||
var generator = new NoiseBasedChunkGenerator(source, registered);
|
||||
var random = RandomState.create(noises, seed, registered.value());
|
||||
var binding = new PopulationGenerationContext.Binding(generator, random);
|
||||
Map<Long, SanctuaryDecorationProtection> protection = new HashMap<>();
|
||||
var terrain = new LostCityTerrain16(island, generator, random, level, position -> {
|
||||
int cx = position.getX() >> 4, cz = position.getZ() >> 4;
|
||||
return protection.computeIfAbsent(key(cx, cz), ignored -> SanctuaryDecorationProtection.create(binding, cx, cz))
|
||||
.protects(position);
|
||||
});
|
||||
var plan = LostCityPlanner16.plan(seed, 0, 0, diameter, true, terrain);
|
||||
int plannedColumns = terrain.columnsUsed();
|
||||
helper.assertTrue(plan.columns() == plannedColumns && plan.columns() <= LostCityPlanner16.MAX_COLUMNS,
|
||||
label(diameter, seed) + " exceeds or bypasses its surface column budget");
|
||||
helper.assertTrue(plan.volumeChecks() <= LostCityPlanner16.MAX_VOLUME_CHECKS,
|
||||
label(diameter, seed) + " exceeds its volume-check budget");
|
||||
var report = verifyPlan(helper, diameter, seed, island, plan, terrain);
|
||||
helper.assertTrue(terrain.columnsUsed() == plannedColumns,
|
||||
label(diameter, seed) + " accepted a foundation that the planner never sampled");
|
||||
report.put("surface_elapsed_ms", (System.nanoTime() - started) / 1_000_000);
|
||||
var underground = surveyUnderground(seed, plan, terrain);
|
||||
report.put("underground", underground);
|
||||
report.put("noise_settings", key.identifier().toString());
|
||||
report.put("columns", plan.columns());
|
||||
report.put("total_columns", terrain.columnsUsed());
|
||||
report.put("volume_checks", plan.volumeChecks());
|
||||
report.put("hydrology_regions", PopulationHydrologyRuntime.cachedRegionCount(random));
|
||||
report.put("hydrology_mask_chunks", protection.size());
|
||||
report.put("native_noise", true);
|
||||
report.put("chunk_requests", 0);
|
||||
report.put("elapsed_ms", (System.nanoTime() - started) / 1_000_000);
|
||||
reports.add(Map.copyOf(report));
|
||||
SanctuaryMod.LOGGER.info("Lost City16 matrix seed {} size {}: {} lots, fallback {}, {} anchored piles, {} columns, {} ms",
|
||||
seed, diameter, report.get("lots"), report.get("fallback"), report.get("foundation_anchors"),
|
||||
plan.columns(), report.get("elapsed_ms"));
|
||||
SanctuaryMod.LOGGER.info("Lost City16 utility survey seed {} size {}: types {}, missing {}, {} modules, {} garage levels, {} total columns, {} UG samples",
|
||||
seed, diameter, underground.get("types"), underground.get("missing_requested_types"),
|
||||
underground.get("modules"), underground.get("garage_levels"), terrain.columnsUsed(), underground.get("samples"));
|
||||
}
|
||||
helper.assertTrue(active.source().islands().equals(originalIslands), "Read-only city matrix changed the live island snapshot");
|
||||
return List.copyOf(reports);
|
||||
}
|
||||
|
||||
/** Mirrors SanctuaryChunkGenerator's generation16 adapter, sharing its sampled columns and
|
||||
* reserving exactly the same piers/architectural foundation before underground prospecting. */
|
||||
private static Map<String, Object> surveyUnderground(long seed, LostCityPlanner16.Result placement, LostCityTerrain16 sampled) {
|
||||
long started = System.nanoTime();
|
||||
int surfaceColumns = sampled.columnsUsed();
|
||||
var supports = new LostCityProtection16.Builder();
|
||||
for (var support : placement.supports()) supports.addViaductSupport(support);
|
||||
if (placement.reservation() != null) supports.addReservation(placement.reservation());
|
||||
var supportMask = supports.build();
|
||||
var underground = LostCityUnderground16.plan(seed, placement.surface(), new LostCityUnderground16.Terrain() {
|
||||
@Override public boolean solid(int x, int y, int z) { return sampled.solid(x, y, z); }
|
||||
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
|
||||
@Override public boolean exteriorAir(int x, int y, int z, int dx, int dz) { return sampled.exteriorAir(x, y, z, dx, dz); }
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
return supportMask.contains(x, y, z) || sampled.protectedAt(x, y, z);
|
||||
}
|
||||
});
|
||||
Map<String, Integer> types = new TreeMap<>();
|
||||
var details = new ArrayList<Map<String, Object>>();
|
||||
int garageLevels = 0, largestGarage = 0;
|
||||
for (var network : underground.networks()) for (var module : network.modules()) {
|
||||
types.merge(module.kind().name(), 1, Integer::sum);
|
||||
var floors = module.parkingFloors();
|
||||
garageLevels += floors.size(); largestGarage = Math.max(largestGarage, floors.size());
|
||||
Map<String, Object> detail = new LinkedHashMap<>();
|
||||
detail.put("kind", module.kind().name());
|
||||
detail.put("bounds", List.of(module.minX(), module.minY(), module.minZ(), module.maxX(), module.maxY(), module.maxZ()));
|
||||
detail.put("floor_y", module.floorY()); detail.put("parking_floors", floors);
|
||||
detail.put("supports", module.supports().size());
|
||||
if (module.kind() == LostCityUnderground16.Kind.SEWER_OUTLET) {
|
||||
var mouth = module.outletMouth();
|
||||
detail.put("outlet_mouth", List.of(mouth.getX(), mouth.getY(), mouth.getZ()));
|
||||
detail.put("outlet_direction", module.outletDirection());
|
||||
}
|
||||
details.add(Map.copyOf(detail));
|
||||
}
|
||||
var requested = List.of(LostCityUnderground16.Kind.SEWER, LostCityUnderground16.Kind.POOLROOM,
|
||||
LostCityUnderground16.Kind.GARAGE, LostCityUnderground16.Kind.EXPANSION_HALL, LostCityUnderground16.Kind.SEWER_OUTLET);
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("mode", "survey"); report.put("networks", underground.networks().size());
|
||||
report.put("types", types); report.put("modules", details.size()); report.put("module_details", details);
|
||||
report.put("missing_requested_types", requested.stream().map(Enum::name).filter(kind -> !types.containsKey(kind)).toList());
|
||||
report.put("garage_levels", garageLevels); report.put("largest_garage_levels", largestGarage);
|
||||
report.put("surface_columns", surfaceColumns); report.put("total_columns", sampled.columnsUsed());
|
||||
report.put("additional_columns", sampled.columnsUsed() - surfaceColumns);
|
||||
report.put("samples", underground.samples()); report.put("sample_limit", LostCityUnderground16.MAX_SAMPLES);
|
||||
report.put("column_limit", LostCityTerrain16.MAX_COLUMNS);
|
||||
report.put("elapsed_ms", (System.nanoTime() - started) / 1_000_000);
|
||||
return Map.copyOf(report);
|
||||
}
|
||||
|
||||
private static Map<String, Object> verifyPlan(GameTestHelper helper, int diameter, long seed, ExpansionIsland island,
|
||||
LostCityPlanner16.Result result, LostCityTerrain16 terrain) {
|
||||
String label = label(diameter, seed);
|
||||
helper.assertTrue(result.surface().districts().size() == 1, label + " must contain exactly one starter town");
|
||||
var district = result.surface().districts().getFirst();
|
||||
helper.assertTrue(district.maxX() - district.minX() + 1 <= 71 && district.maxZ() - district.minZ() + 1 <= 71,
|
||||
label + " escapes the compact starter envelope");
|
||||
Map<Long, Street> streets = new HashMap<>();
|
||||
Set<Long> written = new HashSet<>();
|
||||
var kinds = new TreeMap<String, Integer>();
|
||||
int lots = 0, buildings = 0;
|
||||
for (var parcel : district.parcels()) {
|
||||
kinds.merge(parcel.kind().name(), 1, Integer::sum);
|
||||
boolean street = parcel.kind() == LostCityPlan.Kind.STREET || parcel.kind() == LostCityPlan.Kind.BRIDGE;
|
||||
if (!street) lots++;
|
||||
if (parcel.kind().building()) buildings++;
|
||||
helper.assertTrue(parcel.minY() >= 0 && parcel.maxY() <= 383
|
||||
&& Math.abs((parcel.minX() >> 4) - district.startChunkX()) <= 7
|
||||
&& Math.abs((parcel.maxX() >> 4) - district.startChunkX()) <= 7
|
||||
&& Math.abs((parcel.minZ() >> 4) - district.startChunkZ()) <= 7
|
||||
&& Math.abs((parcel.maxZ() >> 4) - district.startChunkZ()) <= 7,
|
||||
label + " has a parcel outside native height/reference bounds");
|
||||
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) for (int x = parcel.minX(); x <= parcel.maxX(); x++) {
|
||||
helper.assertTrue(written.add(key(x, z)) && island.contains(x, z),
|
||||
label + " has overlapping parcels or an unowned column");
|
||||
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
|
||||
if (result.reservation() == null) {
|
||||
var ground = terrain.ground(x, z);
|
||||
helper.assertTrue(ground.top() <= floor + LostCityPlanner16.MAX_CUT,
|
||||
label + " requires an unapproved natural terrain cut");
|
||||
if (parcel.kind() != LostCityPlan.Kind.BRIDGE) {
|
||||
int bottom = floor - parcel.foundationDepth();
|
||||
helper.assertTrue(ground.dry() && ground.top() >= bottom
|
||||
&& ground.top() - bottom + 1 <= ground.solidDepth(), label + " has an unsupported natural floor");
|
||||
}
|
||||
}
|
||||
if (street) {
|
||||
boolean railing = parcel.kind() == LostCityPlan.Kind.BRIDGE && (parcel.width() > parcel.depth()
|
||||
? z == parcel.minZ() || z == parcel.maxZ() : x == parcel.minX() || x == parcel.maxX());
|
||||
streets.put(key(x, z), new Street(x, z, floor, parcel, !railing));
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(lots >= 3 && buildings >= (lots * 3 + 4) / 5, label + " is not a populated starter town");
|
||||
Set<Long> reached = walk(helper, label, streets);
|
||||
for (var parcel : district.parcels()) if (parcel.kind() != LostCityPlan.Kind.STREET && parcel.kind() != LostCityPlan.Kind.BRIDGE) {
|
||||
helper.assertTrue(parcel.facing() == 0, label + " rotates its entrance away from its certified road");
|
||||
for (int dx = 7; dx <= 9; dx++) {
|
||||
long threshold = key(parcel.minX() + dx, parcel.minZ() - 1);
|
||||
var road = streets.get(threshold);
|
||||
helper.assertTrue(road != null && road.walkable() && road.y() == parcel.floorY() && reached.contains(threshold),
|
||||
label + " has an isolated or obstructed entrance");
|
||||
}
|
||||
for (int x = parcel.minX(); x <= parcel.maxX(); x++)
|
||||
helper.assertTrue(streets.containsKey(key(x, parcel.minZ() - 1)) && streets.containsKey(key(x, parcel.maxZ() + 1)),
|
||||
label + " leaves natural soil between facades and streets");
|
||||
for (int z = parcel.minZ(); z <= parcel.maxZ(); z++)
|
||||
helper.assertTrue(streets.containsKey(key(parcel.minX() - 1, z)) && streets.containsKey(key(parcel.maxX() + 1, z)),
|
||||
label + " lacks a side street beside an admitted lot");
|
||||
}
|
||||
for (var support : result.supports()) for (int z = support.minZ(); z <= support.maxZ(); z++)
|
||||
for (int x = support.minX(); x <= support.maxX(); x++) {
|
||||
var deck = streets.get(key(x, z));
|
||||
helper.assertTrue(deck != null && deck.parcel().kind() == LostCityPlan.Kind.BRIDGE && deck.y() == support.maxY() + 1,
|
||||
label + " has a pier that misses its bridge deck");
|
||||
helper.assertTrue(terrain.solid(x, support.minY(), z) && terrain.solid(x, support.minY() - 1, z),
|
||||
label + " has a viaduct pier without native rock at its foot");
|
||||
}
|
||||
var reservation = result.reservation();
|
||||
if (reservation != null) {
|
||||
helper.assertTrue(reservation.width() == 49 && reservation.depth() == 49 && lots == 4
|
||||
&& reservation.minX() == district.minX() && reservation.maxX() == district.maxX()
|
||||
&& reservation.minZ() == district.minZ() && reservation.maxZ() == district.maxZ(),
|
||||
label + " fallback is not the explicit compact four-lot reservation");
|
||||
for (var anchor : reservation.anchors()) for (int z = anchor.minZ(); z <= anchor.maxZ(); z++)
|
||||
for (int x = anchor.minX(); x <= anchor.maxX(); x++)
|
||||
helper.assertTrue(terrain.solid(x, anchor.minY(), z) && terrain.solid(x, anchor.minY() + 1, z),
|
||||
label + " fallback invents a two-block native rock footing");
|
||||
}
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("generation", 16); report.put("diameter", diameter); report.put("seed", seed);
|
||||
report.put("districts", 1); report.put("lots", lots); report.put("buildings", buildings); report.put("kinds", kinds);
|
||||
report.put("bounds", List.of(district.minX(), district.minZ(), district.maxX(), district.maxZ()));
|
||||
report.put("start_chunk", List.of(district.startChunkX(), district.startChunkZ()));
|
||||
report.put("road_columns", streets.size()); report.put("walkable_road_columns", reached.size());
|
||||
report.put("connected_roads", true); report.put("flush_entrances", true);
|
||||
report.put("fallback", reservation != null); report.put("viaduct_piers", result.supports().size());
|
||||
report.put("foundation_anchors", reservation == null ? 0 : reservation.anchors().size());
|
||||
report.put("passed", true);
|
||||
return report;
|
||||
}
|
||||
|
||||
private static Set<Long> walk(GameTestHelper helper, String label, Map<Long, Street> streets) {
|
||||
Set<Long> walkable = new HashSet<>();
|
||||
streets.forEach((position, street) -> { if (street.walkable()) walkable.add(position); });
|
||||
helper.assertTrue(!walkable.isEmpty(), label + " has no walkable street");
|
||||
Set<Long> reached = new HashSet<>(); var queue = new ArrayDeque<Long>();
|
||||
long start = walkable.iterator().next(); reached.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var current = streets.get(queue.removeFirst());
|
||||
for (int[] delta : new int[][] {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}) {
|
||||
long position = key(current.x() + delta[0], current.z() + delta[1]);
|
||||
var next = streets.get(position);
|
||||
if (next == null || !next.walkable()) continue;
|
||||
helper.assertTrue(Math.abs(current.y() - next.y()) <= 1, label + " contains a street cliff");
|
||||
if (current.parcel() != next.parcel()) helper.assertTrue(current.y() == next.y(),
|
||||
label + " requires an unpainted stair between two independent pieces");
|
||||
if (reached.add(position)) queue.addLast(position);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(reached.equals(walkable), label + " contains disconnected streets");
|
||||
return reached;
|
||||
}
|
||||
private record Street(int x, int z, int y, LostCityPlan.Parcel parcel, boolean walkable) {}
|
||||
private static long key(int x, int z) { return ((long) x << 32) ^ (z & 0xffffffffL); }
|
||||
private static String label(int diameter, long seed) { return "Native city size=" + diameter + " seed=" + seed; }
|
||||
}
|
||||
-1022
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user