Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e8443ad6a | ||
|
|
e3a1438173 | ||
|
|
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
|
||||
-17
@@ -1,17 +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
|
||||
@@ -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.
|
||||
-487
@@ -1,487 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 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,593 +1,130 @@
|
||||
# Sanctuary
|
||||
|
||||
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.
|
||||
|
||||
> 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.
|
||||
|
||||
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.22 est
|
||||
publiée** : le placement des structures redevient sélectif. Les palettes,
|
||||
intérieurs et filons de l’alpha.21 sont conservés. Voir [les essais22](docs/testing-alpha22.md)
|
||||
et [la distribution](docs/packwiz.md).
|
||||
|
||||
La suite est en [conception](docs/structures-conception.md) :
|
||||
départ naturel, secrets et passages souterrains sur l’île, Lost Cities surtout
|
||||
sur les expansions. Les installations découvertes montrent des fonctions
|
||||
reproductibles avec contrôleur, terminal, afficheur, disquettes et blocs vanilla ;
|
||||
un immense laboratoire caché prépare l’accès collectif aux Cavernes. Le train
|
||||
et les rails restent des axes à reconstruire, même sans bâtiments terminaux.
|
||||
L’accès aux raids se découvrira sur une île d’expansion. Boss, portails et raids
|
||||
restent à concevoir. Ce cahier précède la reprise du code ; l’alpha.22
|
||||
reste la livraison décrite ici.
|
||||
|
||||
## 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 |
|
||||
| --- | --- |
|
||||
| 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.21 |
|
||||
|
||||
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.21.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.
|
||||
# Sanctuary — historique des notes de livraison
|
||||
|
||||
Posts historiques après **alpha.30.7**, jusqu’à **beta.110**.
|
||||
|
||||
**Version distribuée : [Sanctuary beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110).** Son post et ses pièces jointes sont conservés.
|
||||
|
||||
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, beta.061, beta.092 et beta.110 utilisent les tags de code exacts déjà publiés.
|
||||
|
||||
**109 posts** sont recensés : beta.001 à beta.110, sauf beta.021, chantier finalisé en beta.023 sans livraison indépendante. Le post beta.076 décrit un lot intégré à beta.077 et précise l’absence de pack beta.076 séparé. L’alpha.30.7 possède déjà son [post](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.7).
|
||||
|
||||
Chaque texte décrit sa version historique ; les livraisons suivantes ont pu modifier les mêmes règles. Les vérifications annoncées viennent des comptes rendus de livraison, sans nouvelle exécution des tests de jeu lors de la publication des notes.
|
||||
|
||||
## Publication
|
||||
|
||||
- **15 septembre 2026 :** 63 posts publiés et vérifiés, 139 pages/liens accessibles, 34 releases antérieures et leurs pièces jointes conservées. Reçu : [publication.json](publication.json).
|
||||
- **17 septembre 2026 :** extension préparée pour beta.063 à beta.110, soit 48 posts dont 45 nouveaux. Les posts beta.063, beta.064 et beta.110 existants sont conservés. [Manifeste](manifest-063-110.json) · [Sources et particularités historiques](sources-063-110.json).
|
||||
|
||||
## Index des posts
|
||||
|
||||
| Version | Notes archivées | Post |
|
||||
| --- | --- | --- |
|
||||
| beta.001 | [Les quatre anciennes expéditions](notes/beta.001.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.001) |
|
||||
| beta.002 | [Biomes boréaux, corail et temples](notes/beta.002.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.002) |
|
||||
| beta.003 | [Créer son habitant et commencer la progression](notes/beta.003.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.003) |
|
||||
| beta.004 | [Cadre Blocodex et sprint](notes/beta.004.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.004) |
|
||||
| beta.005 | [Carte native et noms de couleurs](notes/beta.005.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.005) |
|
||||
| beta.006 | [Brouillard, grille et Demeure](notes/beta.006.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.006) |
|
||||
| beta.007 | [Carte immersive](notes/beta.007.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.007) |
|
||||
| beta.008 | [Minage et vein mining](notes/beta.008.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.008) |
|
||||
| beta.009 | [Construction et vein building](notes/beta.009.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.009) |
|
||||
| beta.010 | [Inventaire jusqu'à six rangées](notes/beta.010.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.010) |
|
||||
| beta.011 | [Factions, prestige et New Game+](notes/beta.011.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.011) |
|
||||
| beta.012 | [Inventaire et progression lisible](notes/beta.012.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.012) |
|
||||
| beta.013 | [Progression, prestige et repères visuels](notes/beta.013.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.013) |
|
||||
| beta.014 | [Rangement, hotbar mobile et préfixe de faction](notes/beta.014.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.014) |
|
||||
| beta.015 | [Icônes de progression communes](notes/beta.015.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.015) |
|
||||
| beta.016 | [Les premiers gestes rapportent de l’XP](notes/beta.016.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.016) |
|
||||
| beta.017 | [Nage, S’allonger, Se reposer et fabrication manuelle](notes/beta.017.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.017) |
|
||||
| beta.018 | [Familiers, cape et cosmétique de tête](notes/beta.018.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.018) |
|
||||
| beta.019 | [Pouvoirs et déplacements des familiers](notes/beta.019.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.019) |
|
||||
| beta.020 | [Œufs singuliers, portage et plongeon](notes/beta.020.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.020) |
|
||||
| beta.022 | [Création de monde plus rapide](notes/beta.022.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.022) |
|
||||
| beta.023 | [Cosmétiques directement sur leur case](notes/beta.023.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.023) |
|
||||
| beta.024 | [Recettes progressives dans le Blocodex](notes/beta.024.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.024) |
|
||||
| beta.025 | [Barres d’XP des compétences](notes/beta.025.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.025) |
|
||||
| beta.026 | [Cache d’affichage Demeure sur la carte](notes/beta.026.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.026) |
|
||||
| beta.027 | [Piles de joueurs](notes/beta.027.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.027) |
|
||||
| beta.028 | [Plongeon jusqu’à collision](notes/beta.028.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.028) |
|
||||
| beta.029 | [Cri et recul des familiers](notes/beta.029.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.029) |
|
||||
| beta.030 | [Lumières portées et objets jetés](notes/beta.030.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.030) |
|
||||
| beta.031 | [Apparition galactique](notes/beta.031.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.031) |
|
||||
| beta.032 | [Refonte des 88 familiers](notes/beta.032.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.032) |
|
||||
| beta.033 | [Menus directs et profil de test rapide](notes/beta.033.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.033) |
|
||||
| beta.034 | [Fil de découvertes et hotbar agrandie](notes/beta.034.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.034) |
|
||||
| beta.035 | [Collections Minecraft](notes/beta.035.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.035) |
|
||||
| beta.036 | [Notifications et suivi choisi](notes/beta.036.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.036) |
|
||||
| beta.037 | [Temps réel et suivi commun](notes/beta.037.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.037) |
|
||||
| beta.038 | [Nourriture, têtes-tombes et factions](notes/beta.038.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.038) |
|
||||
| beta.039 | [Hello World, savoir alimentaire et inventaires de mort](notes/beta.039.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.039) |
|
||||
| beta.040 | [Mode opérateur, fiche Habitant et ciel partagé](notes/beta.040.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.040) |
|
||||
| beta.041 | [Portage des bébés](notes/beta.041.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.041) |
|
||||
| beta.042 | [Joueurs assis sur la tête](notes/beta.042.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.042) |
|
||||
| beta.043 | [Blocs portés et icône officielle](notes/beta.043.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.043) |
|
||||
| beta.044 | [Pouvoirs natifs des familiers](notes/beta.044.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.044) |
|
||||
| beta.045 | [Menu principal](notes/beta.045.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.045) |
|
||||
| beta.046 | [Constellations, vol et zoom](notes/beta.046.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.046) |
|
||||
| beta.047 | [Étoiles à la longue-vue et boutons](notes/beta.047.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.047) |
|
||||
| beta.048 | [Bateaux collectifs et moteurs animaux](notes/beta.048.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.048) |
|
||||
| beta.049 | [Bateaux rectangulaires et chapeaux actifs](notes/beta.049.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.049) |
|
||||
| beta.050 | [Bateaux compacts et redstone portée](notes/beta.050.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.050) |
|
||||
| beta.051 | [Distributeur porté orienté par le regard](notes/beta.051.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.051) |
|
||||
| beta.052 | [Nom du familier de départ](notes/beta.052.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.052) |
|
||||
| beta.053 | [Brume progressive et saturation](notes/beta.053.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.053) |
|
||||
| beta.054 | [Duels, mises et niveaux des familiers](notes/beta.054.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.054) |
|
||||
| beta.055 | [Force et portage de joueurs](notes/beta.055.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.055) |
|
||||
| beta.056 | [Étoiles dispersées et saturation](notes/beta.056.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.056) |
|
||||
| beta.057 | [Météo quotidienne et nouvelles ambiances](notes/beta.057.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.057) |
|
||||
| beta.058 | [Altitude de la carte](notes/beta.058.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.058) |
|
||||
| beta.059 | [Bannières et cartes au trésor](notes/beta.059.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.059) |
|
||||
| beta.060 | [Objets et blocs sur les mobs](notes/beta.060.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.060) |
|
||||
| beta.061 | [Musique d’arrivée et montures familières](notes/beta.061.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.061) |
|
||||
| beta.062 | [Soleil et chapeaux des familiers](notes/beta.062.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.062) |
|
||||
| beta.063 | [Chargement et règle de jeu Sanctuary](notes/beta.063.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.063) |
|
||||
| beta.064 | [Cache et étapes réelles de préparation](notes/beta.064.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.064) |
|
||||
| beta.065 | [Collisions des bateaux collectifs](notes/beta.065.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.065) |
|
||||
| beta.066 | [Cosmétiques animés sans doublon](notes/beta.066.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.066) |
|
||||
| beta.067 | [Commandes du familier et vol du dragon](notes/beta.067.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.067) |
|
||||
| beta.068 | [Animation discrète du chargement](notes/beta.068.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.068) |
|
||||
| beta.069 | [Les ordres reviennent sur H](notes/beta.069.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.069) |
|
||||
| beta.070 | [Textures intégrées et actives par défaut](notes/beta.070.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.070) |
|
||||
| beta.071 | [Personnalités et montures des familiers](notes/beta.071.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.071) |
|
||||
| beta.072 | [Statues et bibliothèque de plans](notes/beta.072.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.072) |
|
||||
| beta.073 | [Duels publics, arènes et paris en objets](notes/beta.073.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.073) |
|
||||
| beta.074 | [Autonomie et ordres des familiers](notes/beta.074.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.074) |
|
||||
| beta.075 | [Bibliothèque de plans centrée sur les fichiers locaux](notes/beta.075.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.075) |
|
||||
| beta.076 | [Navigation — livrée avec beta.077](notes/beta.076.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.076) |
|
||||
| beta.077 | [Tourbillon de nuages sous Sanctuary](notes/beta.077.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.077) |
|
||||
| beta.078 | [Le menu devient Combats](notes/beta.078.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.078) |
|
||||
| beta.079 | [Ordres fiables et défense immédiate en solo](notes/beta.079.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.079) |
|
||||
| beta.080 | [Portée et vitesse liées à la progression](notes/beta.080.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.080) |
|
||||
| beta.081 | [Construction jusqu’à six blocs](notes/beta.081.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.081) |
|
||||
| beta.082 | [Commandes des météos Sanctuary](notes/beta.082.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.082) |
|
||||
| beta.083 | [Menus organisés et découvertes réunies](notes/beta.083.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.083) |
|
||||
| beta.084 | [Clé dorée, Fût et Fourneau](notes/beta.084.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.084) |
|
||||
| beta.085 | [Une façade complète pour le Fourneau](notes/beta.085.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.085) |
|
||||
| beta.086 | [Textures raccordées du Fût](notes/beta.086.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.086) |
|
||||
| beta.087 | [Super pistons normaux et gluants](notes/beta.087.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.087) |
|
||||
| beta.088 | [Navigation du Fût sans recentrer la souris](notes/beta.088.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.088) |
|
||||
| beta.089 | [Correction du crash au contact des super pistons](notes/beta.089.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.089) |
|
||||
| beta.090 | [Passage à Minecraft Java 26.3](notes/beta.090.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.090) |
|
||||
| beta.091 | [Interagir avec les animaux embarqués](notes/beta.091.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.091) |
|
||||
| beta.092 | [Clic molette des super pistons](notes/beta.092.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.092) |
|
||||
| beta.093 | [Recherche créative sans accès graphique en arrière-plan](notes/beta.093.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.093) |
|
||||
| beta.094 | [Les foyers du Fourneau s’allument](notes/beta.094.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.094) |
|
||||
| beta.095 | [Bois raccordé sur la tige des super pistons](notes/beta.095.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.095) |
|
||||
| beta.096 | [Métabli et construction guidée](notes/beta.096.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.096) |
|
||||
| beta.097 | [Temples garantis et aperçus texturés](notes/beta.097.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.097) |
|
||||
| beta.098 | [Construire un plan en créatif](notes/beta.098.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.098) |
|
||||
| beta.099 | [Catalogue visuel du Métabli et gestes K](notes/beta.099.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.099) |
|
||||
| beta.100 | [Exploration, statues découvertes et promenade](notes/beta.100.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.100) |
|
||||
| beta.101 | [Atterrissage sans faux coup pour les volants](notes/beta.101.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.101) |
|
||||
| beta.102 | [Seize couleurs de briques et d’argile](notes/beta.102.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.102) |
|
||||
| beta.103 | [Rangement des couleurs par séries](notes/beta.103.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.103) |
|
||||
| beta.104 | [Argiles grise et noire plus distinctes](notes/beta.104.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.104) |
|
||||
| beta.105 | [Saisons, panoramas et chapeaux vivants](notes/beta.105.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.105) |
|
||||
| beta.106 | [Atelier d’argile et modèles 3D](notes/beta.106.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.106) |
|
||||
| beta.107 | [Œufs de reproduction et de générateurs](notes/beta.107.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.107) |
|
||||
| beta.108 | [La neige s’accumule en blocs](notes/beta.108.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.108) |
|
||||
| beta.109 | [Fonte saisonnière de la neige](notes/beta.109.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/notes/beta.109) |
|
||||
| beta.110 | [Sanctuary beta.110](notes/beta.110.md) | [Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110) |
|
||||
|
||||
@@ -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,285 +0,0 @@
|
||||
# Sanctuary — fiche personnage, initialisation et amitiés
|
||||
|
||||
**Conception WG-26, sans interface ni système social implémenté.** Ce cahier
|
||||
complète les [identités dans le lore](histoire-steve-galactium.md#identités-apparences-et-transformations)
|
||||
et l’[entrée dans la mythologie](histoire-steve-galactium.md#entrer-dans-la-mythologie--accueil-avant-la-première-apparition).
|
||||
|
||||
## Direction demandée
|
||||
|
||||
- **La fiche personnage vient en premier.** Le texte de scénario proposé
|
||||
précédemment est rejeté et retiré de l’accueil : il dévoilait trop le lore.
|
||||
Aucun passage galactique ne précède la fiche.
|
||||
- Le profil montre le **skin et le pseudo actuels**. Le joueur peut écrire une
|
||||
courte bio sur sa pratique de Minecraft ou sur son personnage.
|
||||
- **Une fois la fiche validée**, une petite cinématique figure l’initialisation
|
||||
du personnage, avec une séquence de l’alphabet galactique et une recherche
|
||||
d’emplacement sûr, comme un ordinateur qui démarre.
|
||||
- L’apparition jouable suit cette initialisation, dans la nature de Sanctuary.
|
||||
- **Première apparition sur le serveur : « helloworld {pseudo} ».** Les
|
||||
connexions suivantes conservent le message habituel de connexion. Le critère
|
||||
de première apparition suit l’UUID, tandis que le message montre le pseudo actuel.
|
||||
- Un système d’**amitiés natif** permet de partager la bio publiquement ou avec
|
||||
ses amis. Il doit laisser la possibilité de personnages discrets ou mystérieux.
|
||||
- La présence, l’histoire et la progression sont **liées à l’UUID du compte**
|
||||
dans le monde concerné. Pseudo, skin et bio peuvent évoluer.
|
||||
- Chaque joueur choisit une **couleur personnelle dans la palette du serveur**,
|
||||
**sur sa fiche de création de personnage**. La palette est configurée avant
|
||||
l’arrivée du premier joueur. Une couleur prise devient
|
||||
indisponible aux autres. L’appartenance à une faction précède le pseudo ;
|
||||
le prestige le suit en **chiffres romains**.
|
||||
|
||||
L’ordre est demandé par l’auteur ; les plans, libellés, durées et commandes
|
||||
précises restent proposés. L’archive qui semble se mettre à jour seule reste
|
||||
une piste de fond du lore. L’accueil n’en donne pas d’explication et ne révèle
|
||||
ni les anciens, ni leurs installations, ni la nature de Galactium.
|
||||
|
||||
## Fiche personnage
|
||||
|
||||
Le profil emploie le compte existant ; « créer son personnage » ne signifie
|
||||
ni recréer une identité technique ni choisir une classe obligatoire.
|
||||
|
||||
La fiche présente le skin, le pseudo, le **choix de couleur parmi les teintes
|
||||
disponibles** et un champ **« Votre bio » / « Your bio »**.
|
||||
Suggestion d’invite : **« Qu’aimez-vous faire dans Minecraft ? » / « What do you
|
||||
enjoy doing in Minecraft? »**. Le texte peut aussi être une courte présentation
|
||||
fictionnelle du personnage. Exemple : « Je construis des ponts beaucoup trop
|
||||
grands. Je cherche quelqu’un qui sait faire des ascenseurs. »
|
||||
|
||||
La bio peut rester vide. Sa longueur maximale, sa mise en forme et l’éventuelle
|
||||
présentation de centres d’intérêt restent à choisir. Le texte ne fixe pas des
|
||||
compétences, des récompenses, une faction ou des objectifs obligatoires. Les
|
||||
points communs servent d’abord aux rencontres entre joueurs.
|
||||
|
||||
La présentation conserve les principes du lore : chaque personne peut définir
|
||||
son personnage, sans déduction de genre depuis son skin et sans déclaration
|
||||
de genre ou de transidentité requise. Une bio est un espace d’expression
|
||||
volontaire, pas une demande de renseignements sur la vie réelle.
|
||||
|
||||
La bio peut être complétée plus tard ; valider une fiche avec une bio vide permet de
|
||||
continuer. Les connexions suivantes ne forcent pas à la réécrire. Modifier son
|
||||
profil conserve l’UUID, la progression et les actes associés au personnage.
|
||||
|
||||
## Couleur personnelle, faction et prestige
|
||||
|
||||
**Direction retenue :** composer le nom visible dans l’ordre
|
||||
**faction → pseudo → prestige**. Exemple de présentation :
|
||||
**« Les Égarés Poupoutin VI »**. Le pseudo porte la couleur personnelle ; le
|
||||
préfixe identifie le groupe et permet aussi des associations de noms amusantes.
|
||||
Le choix entre nom, symbole ou combinaison des deux pour la faction reste à
|
||||
dessiner, ainsi que les séparateurs. Le prestige est placé à la suite du pseudo,
|
||||
sur la même ligne, en chiffres romains : **IV pour 4, VI pour 6**. Cette direction
|
||||
remplace l’idée de chiffres galactiques au-dessus du nom.
|
||||
|
||||
### Palette réservée par joueur
|
||||
|
||||
**Dernier choix de l’auteur : une palette préparée pour la population prévue.**
|
||||
Un serveur prévu pour **20 joueurs dispose de 20 couleurs distinctes**, définies
|
||||
avant toute arrivée. Le joueur choisit parmi les couleurs encore disponibles,
|
||||
et sa couleur est retirée des choix des autres joueurs. Le libre choix de
|
||||
n’importe quelle couleur par chaque joueur est remplacé par ce fonctionnement.
|
||||
La palette peut contenir des teintes personnalisées ; elle n’est pas limitée
|
||||
par conception aux couleurs nommées de Minecraft.
|
||||
|
||||
Le choix appartient au profil **UUID dans ce serveur**, indépendamment du
|
||||
pseudo, du skin, du prestige ou de la faction. Une déconnexion ne libère pas
|
||||
la couleur : la palette couvre les habitants, pas seulement les personnes
|
||||
connectées simultanément. Changer de faction conserve donc sa couleur
|
||||
personnelle ; l’emblème ou le préfixe commun indique l’appartenance au groupe.
|
||||
|
||||
**Emplacement retenu : le choix de couleur fait partie de la création du
|
||||
personnage.** Seules les teintes disponibles sont proposées à la sélection.
|
||||
**Détails de parcours proposés :** aperçu du pseudo dans la couleur choisie,
|
||||
puis réservation confirmée par le serveur à la validation de la fiche. Si deux
|
||||
personnes choisissent la même couleur en même temps, une seule l’obtient et
|
||||
l’autre voit les choix actualisés ; ouvrir une fiche ne réserve pas à lui seul
|
||||
une teinte indéfiniment. Un contour lisible autour du texte est proposé pour
|
||||
conserver les teintes choisies sur des décors clairs ou sombres.
|
||||
|
||||
| Élément | Libellé proposé FR / EN |
|
||||
| --- | --- |
|
||||
| Choix | Couleur du joueur / Player color |
|
||||
| État d’une teinte | Disponible / Available ; Déjà choisie / Taken |
|
||||
| Concurrence au choix | Cette couleur vient d’être choisie. / This color was just taken. |
|
||||
|
||||
L’agrandissement d’une palette déjà utilisée, le changement volontaire de
|
||||
couleur et le départ définitif d’un habitant restent à définir. Une palette
|
||||
épuisée ne justifie pas d’attribuer deux fois une couleur ou de reprendre celle
|
||||
d’un joueur absent. Le choix manuel ou la génération assistée de la palette
|
||||
par l’administrateur reste ouvert ; aucune liste de 20 teintes n’est imposée ici.
|
||||
|
||||
### Factions et compteur de prestige
|
||||
|
||||
**Une faction dispose de trois places au départ.** Les prestiges donnent des
|
||||
**charges personnelles liées à l’UUID**, que le joueur peut dépenser dans sa
|
||||
faction pour y débloquer des places. Ces crédits d’agrandissement sont distincts
|
||||
des charges collectives de Galactium. Ils ne transforment pas automatiquement
|
||||
la somme des prestiges des membres en capacité de faction.
|
||||
|
||||
Quantité gagnée par prestige, coût d’une place, permanence des places après
|
||||
le départ d’un membre et éventuel remboursement restent à spécifier dans la
|
||||
[progression](progression-et-integrations.md#compétences-unlocks-galactique-et-prestige).
|
||||
La taille de faction ne détermine pas le nombre de couleurs du serveur.
|
||||
|
||||
Le suffixe représente les prestiges effectivement accomplis, comptés côté
|
||||
serveur et rattachés à l’UUID ; il ne s’agit pas d’un texte libre du profil.
|
||||
Les charges dépensées ne réduisent pas ce compteur. Son affichage n’ajoute
|
||||
aucun pouvoir à la récompense en crédits d’agrandissement.
|
||||
**Propositions d’affichage :** aucun préfixe sans faction, aucun suffixe au
|
||||
prestige zéro ; reprendre cette composition dans le nom au-dessus du joueur,
|
||||
la liste des habitants et le chat, avec une lisibilité à vérifier dans chaque
|
||||
vue. Longueur du préfixe et dessin des symboles restent à concevoir.
|
||||
|
||||
Le pseudo du compte demeure distinct du nom composé. La formule de première
|
||||
apparition reste exactement **« helloworld {pseudo} »** ; cette présentation
|
||||
sociale ne lui ajoute pas implicitement une faction ou un chiffre de prestige.
|
||||
|
||||
## Cinématique d’initialisation après la fiche
|
||||
|
||||
**Intention retenue :** faire sentir le démarrage d’une présence dans le monde
|
||||
par une courte opération visible. La séquence galactique apparaît seulement
|
||||
ici. Elle accompagne l’initialisation et la figuration d’une recherche de point
|
||||
d’entrée, sans nouveau texte de scénario ni traduction donnant une révélation.
|
||||
|
||||
**Découpage proposé, à éprouver visuellement :**
|
||||
|
||||
| Moment | Image et mouvement proposés |
|
||||
| --- | --- |
|
||||
| Fiche validée | La fiche s’efface ; un fond sombre et un curseur ou repère discret prennent le relais. |
|
||||
| Initialisation | Quelques lignes de glyphes galactiques s’inscrivent ou se stabilisent. Le personnage apparaît brièvement comme une silhouette ou un modèle en préparation. |
|
||||
| Recherche d’emplacement | Une grille abstraite ou une petite coupe de terrain parcourt plusieurs positions. Des cases s’éteignent ; un emplacement se stabilise. |
|
||||
| Point d’entrée prêt | Le repère s’immobilise. La transition conduit à la vue du joueur au point choisi sur l’île. |
|
||||
|
||||
La grille n’est pas une carte révélant l’île, les ruines ou les ressources.
|
||||
Les formes, sons et glyphes suggèrent une opération ; le joueur en découvrira
|
||||
la portée plus tard. Le contenu exact de la séquence galactique reste à définir
|
||||
avec le [cahier du langage](langage-et-machines.md). Elle peut figurer des étapes
|
||||
d’initialisation et de positionnement sans être un programme exécutable ni une
|
||||
nouvelle déclaration de Galactium.
|
||||
|
||||
Quelques libellés fonctionnels sont possibles si la lecture des étapes le
|
||||
demande ; ils restent proposés, et la scène peut surtout se lire par l’image :
|
||||
|
||||
| Étape | Proposition FR / EN |
|
||||
| --- | --- |
|
||||
| Préparation | Initialisation / Initializing |
|
||||
| Recherche | Recherche d’un emplacement / Locating an entry point |
|
||||
| Destination validée | Point d’entrée prêt / Entry point ready |
|
||||
|
||||
La recherche est **figurée par la cinématique** ; la sélection et la validation
|
||||
de l’emplacement réel restent à la charge du serveur. L’animation ne vaut pas
|
||||
preuve de sécurité : la fin vers l’apparition attend un point réellement prêt.
|
||||
Elle n’affiche pas de coordonnées inventées ni de pourcentage présenté comme
|
||||
une mesure réelle. Cette distinction reste interne à la conception, sans
|
||||
explication technique à ajouter à l’écran du joueur.
|
||||
|
||||
Durée exacte et synchronisation restent à éprouver ; viser une petite séquence,
|
||||
sans allonger artificiellement une entrée déjà prête. Une option pour passer
|
||||
l’animation est proposée, en attendant tout de même la préparation réelle.
|
||||
La première arrivée est le cas à concevoir d’abord ; répétition ou variante
|
||||
abrégée aux connexions suivantes restent ouvertes. Une reprise ne réinitialise
|
||||
pas la progression conservée sous l’UUID.
|
||||
|
||||
Le raccord à la connexion et au chargement sera défini dans le ticket d’interface.
|
||||
L’arrivée naturelle reste le résultat ; cette scène n’ajoute pas de dimension
|
||||
d’attente ni de déplacement vers une salle construite.
|
||||
|
||||
## Première apparition — helloworld
|
||||
|
||||
**Demandé par l’auteur :** pour la première apparition d’un personnage sur le
|
||||
serveur, remplacer l’annonce de connexion habituelle par
|
||||
**« helloworld {pseudo} »**, par exemple **« helloworld Poupoutin »**.
|
||||
« Après » est compris ici comme les connexions suivantes : elles utilisent
|
||||
l’annonce ordinaire de Minecraft, et non un second message ajouté immédiatement
|
||||
au premier accueil.
|
||||
|
||||
Le message arrive **au moment de l’apparition effective**, après la cinématique
|
||||
et la préparation du point d’entrée. Ouvrir ou valider la fiche, commencer
|
||||
le chargement ou interrompre la connexion avant l’apparition ne suffit pas.
|
||||
Le message visible de première entrée remplace l’annonce ordinaire pour cet
|
||||
événement ; il ne faut pas annoncer une première fois le joueur avant le boot.
|
||||
|
||||
L’historique d’accueil du serveur conserve cette première apparition par **UUID**.
|
||||
Une reconnexion, un redémarrage, une mort, un changement de dimension, de pseudo
|
||||
ou de skin ne la recommence pas. Le pseudo actuel est substitué dans le message ;
|
||||
un compte différent qui reprend un ancien pseudo garde son propre historique.
|
||||
Cette règle ne crée ni progression globale entre serveurs ni remise à zéro du profil.
|
||||
|
||||
| Texte | Statut |
|
||||
| --- | --- |
|
||||
| **helloworld {pseudo}** | Formulation anglaise demandée. |
|
||||
| **helloworld {pseudo}** | Même formule proposée en français, sans préposition ; conserver le clin d’œil « helloworld ». |
|
||||
| Annonce des connexions suivantes | Réutiliser le message habituel de Minecraft et sa localisation. |
|
||||
|
||||
L’enregistrement de la première apparition et l’émission du message doivent
|
||||
être raccordés au même événement serveur, avec une politique de reprise à
|
||||
définir avant code. Les profils déjà présents à l’installation du futur système
|
||||
nécessitent une règle explicite : l’absence du nouveau champ ne prouve pas
|
||||
qu’un ancien joueur vient d’apparaître pour la première fois. Aucun historique
|
||||
existant n’est migré ou réinitialisé par ce cahier.
|
||||
|
||||
## Amitiés et visibilité de la bio
|
||||
|
||||
**Fonctions demandées :** amis intégrés au jeu, bio publique ou réservée aux
|
||||
amis. Proposition de périmètre : les habitants du même serveur, sans publication
|
||||
sur un site ni annuaire global implicites.
|
||||
|
||||
| Réglage proposé | Qui peut consulter la bio ? |
|
||||
| --- | --- |
|
||||
| **Publique / Public** | Les joueurs du serveur. |
|
||||
| **Amis / Friends** | Les amis dont la demande a été acceptée, et l’auteur du profil. |
|
||||
| **Masquée / Hidden — option supplémentaire proposée** | L’auteur seulement ; une bio vide permet aussi de ne rien présenter. |
|
||||
|
||||
Le réglage est visible avant validation du profil. **Défaut proposé : Masquée**,
|
||||
avec choix explicite pour partager. Ce défaut et cette troisième option ne sont
|
||||
pas encore validés. La visibilité concerne la bio ; elle ne masque pas le pseudo
|
||||
ou le skin visibles en jouant.
|
||||
|
||||
**Amitié réciproque proposée :** ouvrir une fiche d’habitant, envoyer une demande,
|
||||
puis attendre son acceptation. Une demande en attente ne donne pas accès aux
|
||||
bios d’amis. Chacun peut refuser ou retirer la relation. Elle est enregistrée
|
||||
entre UUID, avec les noms actuels à l’écran, et ne dépend pas de la présence
|
||||
simultanée des joueurs une fois établie.
|
||||
|
||||
| Action à localiser | Libellé proposé FR / EN |
|
||||
| --- | --- |
|
||||
| Demander une amitié | Ajouter en ami / Add friend |
|
||||
| Répondre | Accepter / Accept ; Refuser / Decline |
|
||||
| Terminer la relation | Retirer des amis / Remove friend |
|
||||
| Changer sa présentation | Modifier le profil / Edit profile |
|
||||
| Entrer en jeu | Entrer dans Sanctuary / Enter Sanctuary |
|
||||
|
||||
La consultation pourrait passer par une liste d’habitants et leurs fiches,
|
||||
avec un accès physique à proximité à étudier. Touches et écrans restent à
|
||||
dessiner avec l’inventaire personnalisé. Devenir ami ouvre l’accès prévu au
|
||||
profil ; permissions sur les coffres, factions, partage de gains et coordonnées
|
||||
conservent leurs règles propres.
|
||||
|
||||
La visibilité est vérifiée **côté serveur avant d’envoyer la bio**, dans toutes
|
||||
ses vues. Retirer une amitié, restreindre la visibilité ou supprimer le texte
|
||||
actualise les accès et les vues du jeu. Cette règle ne peut pas effacer ce
|
||||
qu’une personne a déjà lu. Invitation, acceptation et changement de visibilité
|
||||
restent des actes du joueur, pas des actions automatiques du récit.
|
||||
|
||||
## Personnalités à découvrir
|
||||
|
||||
Une fiche peut raconter des goûts, un projet ou presque rien. Le mystère peut
|
||||
venir d’un personnage peu bavard, d’une bio énigmatique et des installations
|
||||
qu’il laisse découvrir. Le jeu ne complète pas sa biographie à sa place et
|
||||
ne publie pas ses informations privées dans l’accueil ou le lore.
|
||||
|
||||
Les joueurs peuvent ainsi devenir des auteurs reconnaissables dans l’histoire
|
||||
de leur serveur. Cette direction ne donne aucun rôle particulier au créateur
|
||||
du mod dans la fiction, ni ne transforme les anciens en comptes de joueurs.
|
||||
Leur statut et leurs visites gardent le [cadrage du lore](histoire-steve-galactium.md#présences-de-passage).
|
||||
|
||||
Un futur essai devra vérifier l’ordre fiche → initialisation → apparition,
|
||||
l’absence de texte narratif et de glyphes avant la fiche, l’attente d’un
|
||||
emplacement validé par le serveur, une première entrée avec bio vide, une bio publique,
|
||||
une demande d’ami en attente puis acceptée, un retrait et un changement de
|
||||
pseudo/skin conservant le même profil. Pour l’accueil, vérifier un seul
|
||||
« helloworld » à la première apparition effective, aucun en cas d’interruption
|
||||
avant apparition, puis l’annonce ordinaire après reconnexion et redémarrage.
|
||||
Vérifier également une palette de 20 teintes, deux choix simultanés de la même
|
||||
couleur, sa conservation hors ligne et après changement de faction, un profil
|
||||
sans faction ni prestige et le suffixe romain d’un prestige réel.
|
||||
Aucun de ces parcours n’est livré ici.
|
||||
@@ -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).
|
||||
@@ -1,390 +0,0 @@
|
||||
# Redstone 26.3 et informatique Sanctuary
|
||||
|
||||
**Audit de conception du 11 septembre 2026 — WG-26.** Lecture du code Java
|
||||
vanilla exact, des sources Sanctuary et des références historiques, recoupée
|
||||
avec les publications Mojang. Aucun composant, langage, recette ou changement
|
||||
de monde n’est livré par cet audit.
|
||||
|
||||
Suite de conception : le [dossier Redstone Language V0.1](redstone-language-extensions.md)
|
||||
propose désormais les instructions, les fiches matérielles, les périphériques
|
||||
et les composants de signal/transport manquants. Ses nouveaux blocs et valeurs
|
||||
d’essai restent distincts des faits vanilla vérifiés dans cet audit.
|
||||
|
||||
## Ce que Sanctuary change vraiment
|
||||
|
||||
La redstone vanilla permet déjà logique, mémoire, calcul, automatismes et
|
||||
ordinateurs construits avec des blocs. Le contrôleur Sanctuary **concentre ces
|
||||
circuits dans une machine reprogrammable** ; le terminal et les disquettes
|
||||
permettent d’écrire, d’étudier et de partager ses programmes. L’afficheur rend
|
||||
leurs résultats lisibles dans le monde.
|
||||
|
||||
Les nouveaux pouvoirs viennent des **appareils et des informations auxquels le
|
||||
programme accède** : émettre les particules d’un objet, lire précisément un
|
||||
inventaire si une interface le permet, consulter le calendrier Sanctuary, ou
|
||||
demander une expansion à une installation habilitée. Une instruction de calcul
|
||||
n’accorde pas à elle seule ces pouvoirs. Mojang donne déjà les ordinateurs et
|
||||
Minecraft exécuté dans Minecraft comme exemples de créations redstone ; il
|
||||
serait donc inexact de présenter Sanctuary comme l’invention du calcul vanilla.
|
||||
[Redstone Dust — Mojang](https://www.minecraft.net/de-de/article/redstone-dust).
|
||||
|
||||
## Version et portée des constats
|
||||
|
||||
Le projet cible **Minecraft Java 26.3-pre-2**, et non une édition Bedrock ni
|
||||
une version finale supposée. Le JAR local indique `stable: false`, une compilation
|
||||
du 4 septembre 2026, Java 25 et data pack 120. La pre-release 3, publiée le
|
||||
8 septembre, est postérieure à cette cible ; son existence ne met pas le pack
|
||||
à jour. La pre-2 corrige notamment la détection de fonte de neige/glace par les
|
||||
capteurs sculk ; la pre-3 touche notamment les calculs de number providers.
|
||||
[Pre-release 2](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-2),
|
||||
[pre-release 3](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-3).
|
||||
|
||||
L’inventaire ci-dessous couvre les familles de composants, leurs mesures et
|
||||
leurs effets utiles à la conception. Il ne prétend pas énumérer tous les circuits
|
||||
que les joueurs peuvent composer, ni valider chaque montage en jeu. Les nombres
|
||||
précis viennent des classes 26.3-pre-2 identifiées en fin de document. Les
|
||||
publications d’anciennes versions servent de contexte ; leur comportement a été
|
||||
recoupé avec cette cible lorsqu’il est détaillé ici.
|
||||
|
||||
## Le modèle vanilla : signal, temps et matière
|
||||
|
||||
Une liaison redstone ordinaire porte une intensité entière **de 0 à 15**. Elle
|
||||
peut représenter un booléen, un niveau, ou une valeur codée par le constructeur.
|
||||
Elle ne transporte pas spontanément un nom d’objet, un texte ou une identité
|
||||
de joueur. Un montage peut transmettre davantage par plusieurs lignes ou par
|
||||
une séquence dans le temps ; cela exige un encodage et un récepteur.
|
||||
|
||||
La poudre perd un niveau en propageant le signal d’une poudre à la suivante.
|
||||
Le répéteur réémet une sortie à 15 lorsque son entrée est active, avec un délai
|
||||
réglable de **2, 4, 6 ou 8 ticks de jeu**. Les blocs conducteurs, les directions
|
||||
et les alimentations fortes/faibles font partie du circuit : deux blocs voisins
|
||||
ne sont pas automatiquement deux appareils communicants.
|
||||
|
||||
La simulation vise **20 ticks de jeu par seconde** au réglage normal. Un délai
|
||||
de circuit suit cette simulation, avec ses pauses et ralentissements. Le futur
|
||||
calendrier réel de Sanctuary est une autre source de temps : un jour de vingt-
|
||||
quatre heures ne doit pas rendre chaque répéteur soixante-douze fois plus lent.
|
||||
Un chronomètre devra préciser s’il mesure du temps simulé ou du temps réel.
|
||||
|
||||
La redstone est une commande, sans réserve d’énergie consommée à chaque impulsion.
|
||||
Un four consomme son combustible et le crafter ses ingrédients ; la puissance
|
||||
du signal ne les fournit pas. Le contrôleur devra de même distinguer calcul,
|
||||
commande et consommation matérielle.
|
||||
|
||||
## Les entrées et mesures disponibles
|
||||
|
||||
| Famille vanilla | Ce que le circuit reçoit | Ce qu’il faut préserver dans Sanctuary |
|
||||
| --- | --- | --- |
|
||||
| Levier, boutons | État maintenu ou impulsion déclenchée par interaction ; certains boutons réagissent aussi aux projectiles. | Une commande locale n’identifie pas automatiquement son auteur. |
|
||||
| Plaques de pression ordinaires et pondérées | Présence ou niveau dépendant des entités selon le type de plaque. | Un compte d’entités n’est pas une liste de joueurs. |
|
||||
| Fil de détente et crochets | Détection sur une ligne installée. | Le parcours et les attaches ont une utilité physique. |
|
||||
| Coffre piégé | Signal lié au nombre d’utilisateurs qui l’ouvrent, dont les joueurs et les golems de cuivre ; son inventaire a aussi une mesure de comparateur. | État d’ouverture et contenu sont deux informations différentes ; ce signal ne prouve pas une présence humaine. |
|
||||
| Cible | Intensité liée à la précision de l’impact. | Un score de tir peut commencer par une valeur analogique, sans attribution personnelle implicite. |
|
||||
| Détecteur de lumière du jour, mode inversé | Valeur issue de l’éclairage du ciel et de l’angle solaire. | Ce n’est pas une horloge civile ni un capteur générique de toute lumière artificielle. |
|
||||
| Paratonnerre | Impulsion lors d’un impact de foudre. | Pas un capteur continu de pluie. |
|
||||
| Observateur | Impulsion lors d’une mise à jour détectée sur la face observée. | Il ne transmet ni le contenu complet du bloc ni une explication de son changement. |
|
||||
| Capteur sculk | Vibration reçue, puissance liée à la distance ; comparateur pour la fréquence de l’événement pendant l’activité. | Portée, trajet, occlusion et états du capteur comptent ; les événements ne donnent pas une identité complète. |
|
||||
| Capteur sculk calibré | Filtrage par fréquence avec une entrée de calibration à l’arrière ; plus grande portée. | Une entrée électrique peut paramétrer un capteur sans lui fournir un programme. |
|
||||
| Rail détecteur | Présence d’un wagon et lecture analogique de certains wagons. | Le transport lui-même peut servir de support d’information. |
|
||||
|
||||
Le code donne une portée de **8 blocs** au capteur sculk ordinaire et **16** au
|
||||
calibré. L’activité dure respectivement **30 et 10 ticks**, puis le refroidissement
|
||||
dure **10 ticks**. Le calibré filtre la fréquence lorsque son entrée de
|
||||
calibration est non nulle. Ces mesures ne remplacent pas un capteur Sanctuary
|
||||
de statistiques ou d’advancements.
|
||||
|
||||
La réception d’une vibration attend aussi que les neuf chunks du carré 3×3
|
||||
autour du capteur soient présents et autorisés à simuler les blocs. Le trajet
|
||||
prend `floor(distance)` ticks. La laine peut occulter la vibration et certaines
|
||||
actions accroupies sont ignorées. Ce dispositif n’est pas un journal exhaustif
|
||||
des actions des joueurs.
|
||||
|
||||
### Le comparateur mérite un inventaire à lui seul
|
||||
|
||||
Le comparateur compare son entrée arrière aux entrées latérales, ou soustrait
|
||||
la plus forte entrée latérale. En lecture de bloc, le sens dépend du bloc lu.
|
||||
Il peut déjà relier des objets usuels à des serrures, jauges et machines.
|
||||
[Présentation Mojang du comparateur](https://www.minecraft.net/nb-no/article/taking-inventory--redstone-comparator).
|
||||
|
||||
| Bloc ou objet lu | Information disponible |
|
||||
| --- | --- |
|
||||
| Coffres, coffres en cuivre, tonneaux, shulkers, hoppers, droppers, dispensers, fours et alambics | Remplissage de l’inventaire, pondéré par capacité d’empilement ; pas le nom ni la quantité exacte de chaque objet. |
|
||||
| Pot décoré | Remplissage de son inventaire. |
|
||||
| Crafter | Nombre d’emplacements occupés **ou désactivés**, de 0 à 9 ; ce n’est pas une mesure standard de stacks pleines. |
|
||||
| Bibliothèque sculptée | Dernier emplacement manipulé, pas le texte des livres. |
|
||||
| Étagère — shelf | Présence des trois cases encodée par les poids 1, 2 et 4, soit 0–7, lisible à l’arrière ; ni quantité ni identité. |
|
||||
| Pupitre | Position de la page dans le livre, pas son texte ; tourner la page émet aussi une impulsion. |
|
||||
| Cadre et cadre lumineux | Présence et rotation de l’objet. |
|
||||
| Jukebox | Valeur définie par la chanson ; la lecture du disque produit aussi un signal d’activité. |
|
||||
| Gâteau et gâteau avec bougie | État du gâteau, donc parts restantes. |
|
||||
| Composteur | Niveau de compostage. |
|
||||
| Chaudrons d’eau/neige poudreuse/lave | Niveau ou état de remplissage prévu par la variante. |
|
||||
| Ruche et nid d’abeilles | Niveau de miel. |
|
||||
| Ancre de réapparition | Charge disponible. |
|
||||
| Cadre de portail de l’End | Présence d’un œil ; le bloc n’est pas une pièce fabriquable librement en survie. |
|
||||
| Ampoule en cuivre | État allumé mémorisé, sortie 15 ou 0. |
|
||||
| Statue de golem de cuivre | Pose debout = 1, assise = 2, course = 3, étoile = 4. |
|
||||
| Cœur de Creaking | Distance à son Creaking associé : `15 − floor(clamp(distance, 0, 32) / 32 × 15)` ; 0 sans associé ou à l’état déraciné. |
|
||||
| Capteur sculk, rail détecteur | Fréquence de vibration ou donnée du wagon pris en charge. |
|
||||
| Bloc de commande | Nombre de succès de la commande ; relève des outils administrateur, pas d’un composant de progression en survie. |
|
||||
|
||||
Pour les inventaires ordinaires, la mesure est **0 si vide**, sinon
|
||||
`floor(14 × remplissage moyen) + 1`. Le remplissage tient compte du nombre de
|
||||
cases et de la capacité des stacks. Deux contenus différents peuvent donc donner
|
||||
la même intensité. Une jauge Sanctuary branchée seulement sur un comparateur
|
||||
doit présenter cette mesure, sans inventer un stock exact à partir de celle-ci.
|
||||
|
||||
Les étagères et statues, apparues avec The Copper Age, sont bien présentes dans
|
||||
la cible 26.3. Une étagère alimentée permet, lors de l’interaction du joueur,
|
||||
d’échanger plusieurs emplacements avec sa barre rapide ; jusqu’à trois étagères
|
||||
connectées permettent l’échange de cette barre. La redstone seule n’effectue
|
||||
pas le clic du joueur. Ces objets offrent déjà des interfaces diégétiques.
|
||||
[The Copper Age — publication Java](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-9).
|
||||
|
||||
## Ce que la redstone permet de construire et d’actionner
|
||||
|
||||
| Famille | Capacité vanilla | Limite ou nuance utile |
|
||||
| --- | --- | --- |
|
||||
| Poudre, blocs conducteurs, bloc de redstone | Transport et source constante de signal. | Intensité et orientation ; pas un câble de données riches par défaut. |
|
||||
| Torches, répéteurs et comparateurs | Inversion, seuil, soustraction, délai, isolation, verrouillage. | Leur composition permet portes logiques, additionneurs, compteurs et séquenceurs. |
|
||||
| Boucles et mémoires construites | Horloges, impulsions, bascules, registres, stockage et machines de calcul. | Emprise, temps de propagation et mises à jour réelles. La liste des constructions possibles n’est pas fermée. |
|
||||
| Ampoule en cuivre | Bascule d’état à chaque nouvelle alimentation, état conservé sans signal maintenu. | Source lumineuse et mémoire simple ; elle ne module pas sa luminosité selon 1–15 comme le débit du particuleur. |
|
||||
| Pistons, pistons collants, slime et miel | Déplacer des blocs, ouvrir des portes, déplacer des entités et construire des machines mobiles. | Limite de poussée de 12 blocs ; blocs non déplaçables et réactions spécifiques. La cible Java refuse le déplacement des block entities. |
|
||||
| Portes, trappes, portillons | Commander des passages, des accès et des montages utilisant l’eau. | Les variantes gardent leurs règles d’interaction ; pas de téléportation générique. |
|
||||
| Rails ordinaires, propulseurs, détecteurs et activateurs | Aiguiller, déplacer/freiner, détecter ou agir sur les wagons adaptés. | Chaque type remplit un rôle ; les occupants et inventaires restent physiques. |
|
||||
| Hopper et wagons à inventaire | Transporter, aspirer et distribuer des items ; verrouiller un hopper par redstone. | Faces, inventaires admissibles, temps de transfert et capacité réelle. |
|
||||
| Dropper | Tenter de déplacer un item vers l’inventaire en face ; sans inventaire, le lâcher dans le monde. | Si l’inventaire trouvé refuse l’insertion, l’item reste dans le dropper. Ne reproduit pas toutes les actions d’un clic droit. |
|
||||
| Dispenser | Exécuter une action définie pour l’objet : projectiles, seaux, poudre d’os, allumage, tonte, etc. | L’objet doit avoir un comportement prévu ; il ne sait pas utiliser arbitrairement tout nouveau bloc. |
|
||||
| Crafter | Tenter une fabrication à chaque nouvelle impulsion, à partir des ingrédients de ses neuf cases. | Une recette valide est nécessaire. La grille peut être configurée ; l’ordinateur ne serait pas l’invention de l’auto-craft. |
|
||||
| Fours, alambics, composteurs et fermes | Chaînes de transformation alimentées et surveillées ; récolte par combinaisons mécaniques. | Leurs recettes, combustibles, temps et mécanismes de croissance restent nécessaires. |
|
||||
| Lampes, blocs musicaux, jukebox, cloche | Voyants, sons, musique, alarmes et animations construites. | Son, lumière et durée ont leurs propres règles. Un signal de bloc musical ne choisit pas sa note par une valeur arbitraire. |
|
||||
| TNT et projectiles | Déclenchement d’explosions, propulsion, pièges et récoltes selon le montage. | Consommation, dégâts et règles de drops ; le calcul ne crée pas gratuitement la matière. |
|
||||
| Eau, bulles, portails, perles de l’End | Transport et mécanismes indirects que des circuits peuvent déclencher ou organiser. | Pas d’opération vanilla ordinaire « générer un continent à ces coordonnées ». |
|
||||
| Allays, villageois, golems de cuivre | Collecte, récolte ou tri matériel selon les comportements de ces entités. | Ce sont des acteurs du montage, pas des CPU adressables par un programme vanilla. |
|
||||
|
||||
Le crafter utilise déjà la chaîne **ingrédients → impulsion → résultat**, avec
|
||||
des hoppers possibles en alimentation. Le contrôleur pourra gérer le moment
|
||||
et la quantité de production ; il n’a pas besoin de remplacer ce bloc.
|
||||
[Guide Mojang du crafter](https://www.minecraft.net/en-us/article/crafting-crafter).
|
||||
|
||||
Quelques cadences vérifiées : le dropper et le crafter programment leur action
|
||||
quatre ticks après une nouvelle alimentation ; un signal maintenu ne suffit
|
||||
pas à les faire répéter indéfiniment. Le hopper a un refroidissement de huit
|
||||
ticks après un transfert réussi ; il peut pousser puis aspirer dans un même
|
||||
cycle, et un ramassage au sol peut intégrer une pile. Il serait inexact de
|
||||
résumer tous ces appareils à « un item par tick ».
|
||||
|
||||
Le golem de cuivre recherche ses coffres jusqu’à 32 blocs horizontalement et
|
||||
huit verticalement, prend au plus 16 objets, et dépend d’un chemin praticable.
|
||||
Il prélève dans les coffres en cuivre et dépose dans des coffres ordinaires ou
|
||||
piégés vides ou contenant le même type. Ce tri est déjà une capacité vanilla ;
|
||||
les futurs services payants des villageois de Sanctuary sont un autre chantier.
|
||||
|
||||
Le dispenser 26.3-pre-2 a des comportements explicites pour les projectiles,
|
||||
les bateaux et wagons, les seaux, le briquet, la poudre d’os, la TNT, certaines
|
||||
constructions d’entités, les shulkers, bouteilles, glowstone, cisailles, brosse,
|
||||
rayons de miel et certaines potions. Les objets équipables ont également leurs
|
||||
règles. Cette liste de comportements spécialisés explique pourquoi **accepter
|
||||
un item par dropper** et **être activé par un dispenser** sont deux contrats.
|
||||
|
||||
### Les particularités Java à ne pas effacer
|
||||
|
||||
Les mises à jour de blocs et leur ordre font partie du fonctionnement des
|
||||
circuits. La quasi-connectivité existe notamment pour pistons et
|
||||
dispensers/droppers : l’alimentation peut être détectée au-dessus, avec des
|
||||
conditions de mise à jour. Cela ne signifie pas que tous les nouveaux appareils
|
||||
doivent l’adopter. Leurs faces, leur émission et leur réaction aux impulsions
|
||||
devront être spécifiées et essayées avec des circuits vanilla.
|
||||
|
||||
Le code contient un évaluateur de poudre alternatif derrière
|
||||
`FeatureFlags.REDSTONE_EXPERIMENTS`. Sa présence dans le JAR ne prouve pas que
|
||||
tous les mondes l’activent. L’audit ne modifie aucune option expérimentale.
|
||||
L’activité dépend également des chunks et des entités effectivement simulés ;
|
||||
la présence d’un ordinateur ne donne pas automatiquement un chargement permanent.
|
||||
|
||||
### Commandes et data packs : une autre couche du vanilla
|
||||
|
||||
Les commandes, fonctions de data packs, scoreboards et entités d’affichage
|
||||
permettent déjà du calcul, du texte dynamique, des particules et des effets
|
||||
sur le monde. Il serait faux d’affirmer que Minecraft ne sait jamais faire ces
|
||||
choses. Mais leur mise en place relève de l’auteur de carte ou de l’administration,
|
||||
pas d’un ordinateur fabriqué puis programmé librement par un joueur en survie.
|
||||
Dans la cible, `/particle` et `/compute` exigent la permission de game master ;
|
||||
l’édition d’un bloc de commande vérifie aussi ce droit.
|
||||
|
||||
La différence Sanctuary est de rendre certaines possibilités **constructibles,
|
||||
apprenables et reproductibles dans la partie**, à travers des appareils avec
|
||||
leurs règles. Le Redstone Language n’est pas une console de commandes opérateur.
|
||||
|
||||
## Comparaison avec le socle Sanctuary
|
||||
|
||||
| Élément Sanctuary | Ce qu’il rend plus pratique | Ce qu’il ajoute réellement | Statut |
|
||||
| --- | --- | --- | --- |
|
||||
| Contrôleur | Logique, compteurs, conditions, séquences, mémoire et calcul en peu de blocs. | Programme modifiable, processeur et RAM intégrés, six faces d’E/S. | Direction retenue ; architecture et cadence à concevoir. |
|
||||
| Terminal | Concevoir, examiner et corriger un montage logique. | Éditeur d’assembleur, assemblage, inspection de RAM/registres et chargement de programmes. | Retenu ; pas de bloc Assembleur séparé. |
|
||||
| Disquette | Réutiliser et transmettre une solution. | Programme porté par un objet que l’on peut trouver, copier et échanger. | Retenu ; recettes, capacité et copie à définir. |
|
||||
| Afficheur | Lire compteurs, jauges et états sans une grande matrice de lampes. | Texte dynamique programmable, écran rectangulaire connecté, huit lignes par bloc en hauteur. | Retenu ; connexion aux données et limites à concevoir. |
|
||||
| Particuleur | Produire des effets décoratifs contrôlés dans une construction. | Objet-échantillon choisissant les particules ; signal réglant leur débit. | Direction précisée ci-dessous ; pas de portage livré. |
|
||||
| Lecture précise d’inventaire | Comptage, tri et coordination de production. | Accès structuré aux types et quantités, au-delà du comparateur. | Interface à concevoir ; pas un effet automatique de la RAM. |
|
||||
| Calendrier et progression | Tableaux, objectifs, événements et suivi. | Données serveur Sanctuary consultables par des programmes. | Sources et droits à concevoir ; un capteur physique ne les invente pas. |
|
||||
| Installation Galactium | Orchestrer un projet collectif. | Demande d’expansion avec conditions matérielles, charge collective et résultat persistant. | Moteur d’expansion existant ; raccord jouable au programme absent. |
|
||||
| Convoyeurs, stockage titane, Chunky | Logistique visible, stockage consultable et activité permanente choisie. | Appareils supplémentaires avec matériaux, portée et progression propres. | Vision future ; ne sont pas acquis par le seul achat du contrôleur. |
|
||||
|
||||
Le langage de calcul envisagé est un assembleur **8 bits**, avec RAM adressable.
|
||||
Les valeurs internes 0–255 ne changent pas les faces électriques 0–15. Texte,
|
||||
coordonnées et gros compteurs demandent une représentation ou un protocole
|
||||
adapté. Un CPU 8 bits n’impose pas une RAM de 256 octets ; sa capacité reste à
|
||||
choisir dans le [cahier informatique](langage-et-machines.md).
|
||||
|
||||
La sauvegarde proposée porte sur l’état complet : programme, position d’exécution,
|
||||
registres, RAM et attentes. Les anciens ordinateurs locaux sauvegardaient une
|
||||
partie de cet état, mais recréaient le CPU au début de chaque tick. Ils sont
|
||||
des références à reprendre, pas une preuve que la nouvelle exécution continue
|
||||
est déjà disponible. Voir la [lecture du Redstone Computer](redstone-computer-reference.md).
|
||||
|
||||
## Trois échanges à définir séparément
|
||||
|
||||
| Échange | Exemple | Contrat |
|
||||
| --- | --- | --- |
|
||||
| Signal redstone | Le contrôleur sort 7 vers le particuleur, ou une impulsion vers un dropper. | Faces, valeur 0–15, maintien/impulsion, cadence et mises à jour. |
|
||||
| Objet physique | Un hopper/dropper apporte un échantillon au particuleur. | Inventaire source, place libre, faces d’insertion/extraction et transfert effectif. |
|
||||
| Donnée ou commande de périphérique | Le contrôleur écrit du texte sur l’afficheur ou lit un inventaire exact. | Connexion identifiée, données accessibles, résultat et actions permises. Ce protocole reste à concevoir. |
|
||||
|
||||
Le minimum peut utiliser une connexion adjacente entre appareils compatibles,
|
||||
sans imposer un nouveau bloc de bus. Une communication riche par poudre ou entre
|
||||
contrôleurs distants demeure possible à concevoir, mais exige son propre protocole.
|
||||
Un signal redstone ne transporte pas à lui seul un `ItemStack`.
|
||||
|
||||
Exemple : **coffre → comparateur → contrôleur → particuleur**. Le programme peut
|
||||
utiliser l’intensité reçue pour régler le débit de fumée. Il connaît alors un
|
||||
niveau de remplissage, pas « 128 lingots de fer ». Cette dernière information
|
||||
demanderait une lecture explicite de l’inventaire, donc une capacité supplémentaire.
|
||||
|
||||
## Particuleur : contrat clair et montages
|
||||
|
||||
**Retenu : un objet inséré au clic droit choisit les particules. Plus le signal
|
||||
est fort, plus l’appareil émet de particules.** Le contrat de travail utilise
|
||||
0 pour arrêter, puis un débit croissant de 1 à 15. La courbe, la cadence maximale
|
||||
et l’orientation de sortie restent à définir. Cette variation concerne le
|
||||
nombre de particules dans le temps ; elle n’impose pas un changement de taille,
|
||||
de vitesse ou de portée.
|
||||
|
||||
L’insertion par **dropper** est demandée. Le **hopper accolé** est le candidat
|
||||
vanilla proposé pour l’alimentation continue. Le contrôleur peut régler le
|
||||
débit par une face électrique ordinaire, commander le dropper, ou verrouiller
|
||||
le hopper. Un éventuel transfert direct par le contrôleur demanderait une
|
||||
fonction de manipulation d’inventaire ; il ne fait pas apparaître un objet.
|
||||
|
||||
**Proposition minimale : un emplacement d’échantillon.** Clic droit pour insérer
|
||||
ou remplacer en rendant l’ancien objet, main vide pour retirer. Je recommande
|
||||
de conserver l’échantillon pendant l’émission pour en faire un matériau de
|
||||
réglage d’un appareil décoratif ; sa consommation n’est pas encore décidée.
|
||||
Pour changer de particule automatiquement, prévoir une extraction puis une
|
||||
insertion réelles. Le transfert ne doit pas détruire l’échantillon précédent.
|
||||
|
||||
Le code historique `26.2/redstoner` possède déjà une table de correspondances,
|
||||
un slot, le remplacement manuel et l’insertion automatisée. Il **refuse
|
||||
l’extraction automatisée** ; reprendre cette règle telle quelle bloquerait le
|
||||
changement automatique d’échantillon. Il augmente aussi vitesse et fréquence,
|
||||
et produit un nuage par défaut, même sans échantillon : ces détails historiques
|
||||
ne sont pas validés pour le nouveau particuleur.
|
||||
|
||||
| Échantillon | Correspondance historique utilisable comme proposition |
|
||||
| --- | --- |
|
||||
| Charbon de bois | Fumée. |
|
||||
| Bloc musical | Notes. |
|
||||
| Perle de l’End | Particules de portail. |
|
||||
| Mousse | Spores. |
|
||||
|
||||
La nouvelle table doit être explicite et extensible ; elle ne suppose pas que
|
||||
chaque item possède une unique « particule naturelle ». **Proposition : vide =
|
||||
aucune émission**, et règle explicite pour les objets non reconnus. Un comparateur
|
||||
de présence pourrait être utile ; il resterait facultatif. Les effets sont
|
||||
proposés comme visuels : une particule de flamme n’allumerait pas un incendie.
|
||||
|
||||
Les scènes possibles sont simples à composer :
|
||||
|
||||
- un atelier émet plus de fumée quand un stock ou une activité augmente ;
|
||||
- une arrivée déclenche une courte émission par plaque de pression ;
|
||||
- une installation montre attente, activité et fin par plusieurs émetteurs ;
|
||||
- un programme séquence musique, lampes et particules pour un spectacle ;
|
||||
- un réseau d’items change l’échantillon, tandis que le programme règle le débit.
|
||||
|
||||
Le particuleur doit rester utilisable avec un levier ou un comparateur seul.
|
||||
Le contrôleur apporte les variations dans le temps et la coordination. Les
|
||||
ruines peuvent montrer ces usages en fonctionnement, sans panneau explicatif.
|
||||
|
||||
## Où garder la puissance de construction
|
||||
|
||||
**Recommandation :** les programmes calculent et coordonnent ; les blocs et
|
||||
installations conservent les opérations physiques. Le contrôleur choisit quand
|
||||
fabriquer ; le crafter fabrique avec ses ingrédients. Le contrôleur compte les
|
||||
impulsions ; le hopper déplace les objets. Le contrôleur choisit un débit ; le
|
||||
particuleur émet. Une interface supplémentaire peut enrichir cette relation,
|
||||
mais ses possibilités doivent être explicites.
|
||||
|
||||
Cela rend la programmation puissante sans faire disparaître les chemins,
|
||||
coffres, rails, fermes et ateliers que les joueurs aiment construire. De petites
|
||||
machines restent utiles sans ordinateur ; plusieurs contrôleurs peuvent former
|
||||
une installation plus élaborée lorsque leurs connexions sont définies.
|
||||
|
||||
Pour Galactium, le programme pourra calculer et soumettre une demande ; le
|
||||
serveur devra vérifier installation, charge collective, ressources et emprise.
|
||||
Le moteur actuel possède réservation, préparation progressive et reprise ; il
|
||||
ne fournit pas encore ce parcours de survie. Une instruction ne remplace pas
|
||||
les étapes de progression et ne permet pas de réécrire librement les blocs ou
|
||||
les sauvegardes. Voir le [raccord d’expansion](langage-et-machines.md#ce-que-le-moteur-fournit-déjà-et-ce-qui-manque).
|
||||
|
||||
## Points à éprouver avant livraison
|
||||
|
||||
Le premier essai jouable pourrait réunir un contrôleur, un terminal, une
|
||||
disquette, un coffre avec comparateur, un afficheur et un particuleur. Il
|
||||
vérifierait un programme lisible et copiable, une jauge honnête, un débit modulé
|
||||
et la reprise de son état. Le particuleur doit aussi réussir son essai autonome.
|
||||
Ce parcours est proposé ; il ne lance pas de nouvelle livraison.
|
||||
|
||||
Les règles à arrêter portent sur les transitions de signal, les connexions,
|
||||
la cadence CPU, les transferts, la reprise et les états d’erreur. Calculer
|
||||
beaucoup d’instructions et déplacer beaucoup d’objets sont deux coûts distincts.
|
||||
Une émission décorative doit également rester supportable pour les clients :
|
||||
limites par appareil et par zone, synchronisation et génération visuelle à définir.
|
||||
Ni le programme ni le particuleur ne donnent un chargement de chunk permanent ;
|
||||
ce dernier reste lié au chantier Chunky et clés.
|
||||
|
||||
## Traces de lecture et limites
|
||||
|
||||
La lecture du code actif utilise `sanctuary-beta`, branche `codex/blocodex-natif`,
|
||||
commit **e654d56**. Le travail documentaire est isolé sur
|
||||
`codex/conception-territoire`. Le code actif enregistre notamment monde,
|
||||
expansions, Blocodex et recensements ; il n’enregistre pas le nouveau trio
|
||||
informatique ni le particuleur. Aucune comparaison de performances ni simulation
|
||||
de circuit n’a été exécutée pour cet audit.
|
||||
|
||||
Le JAR de sources généré localement par Loom est
|
||||
`.gradle/loom-cache/minecraftMaven/net/minecraft/minecraft-merged-6974b2190e/26.3-pre-2/minecraft-merged-6974b2190e-26.3-pre-2-sources.jar`
|
||||
dans le dépôt de code. Les classes ont été lues en mémoire, sans import de sources
|
||||
vanilla dans Git. Le JAR déobfusqué de contrôle est
|
||||
`~/.gradle/caches/fabric-loom/minecraftMaven/net/minecraft/minecraft-merged-deobf/26.3-pre-2/minecraft-merged-deobf-26.3-pre-2.jar`,
|
||||
SHA-256 `980d5ffa0de0e2fe784a377858053d228dd56e0b0a59505a96a5b058cdac09ef`.
|
||||
|
||||
| Preuve locale, sous `net/minecraft/` dans les sources vanilla | Constats associés |
|
||||
| --- | --- |
|
||||
| `world/level/SignalGetter` ; sous `world/level/` : `block/RedstoneWireBlock`, `redstone/RedstoneWireEvaluator`, `block/RepeaterBlock`, `block/DiodeBlock` | Intensité, propagation, sens, délais et évaluateur expérimental. |
|
||||
| `world/TickRateManager` | Cadence de simulation normale, réglable. |
|
||||
| Sous `world/level/block/piston/` : `PistonBaseBlock`, `PistonStructureResolver` | Quasi-connectivité, limite de 12 et refus des block entities. |
|
||||
| Sous `world/level/block/` : `ObserverBlock`, `RedstoneTorchBlock`, `DaylightDetectorBlock`, `CopperBulbBlock` | Détection, inversion, mesure solaire et bascule lumineuse. |
|
||||
| `world/inventory/AbstractContainerMenu`, `world/level/block/ComparatorBlock` et méthodes `getAnalogOutputSignal` des blocs recensés | Calcul de remplissage et sources de comparateur. |
|
||||
| Sous `world/level/block/` : `SculkSensorBlock`, `CalibratedSculkSensorBlock` ; sous `world/level/block/entity/` : `SculkSensorBlockEntity`, `CalibratedSculkSensorBlockEntity` ; `world/level/gameevent/vibrations/VibrationSystem` | Portées, filtrage, temps d’activité et conditions de réception. |
|
||||
| `world/level/block/DropperBlock`, `world/level/block/DispenserBlock`, `world/level/block/entity/HopperBlockEntity`, `core/dispenser/DispenseItemBehavior` | Transferts, impulsions, faces et usages spécialisés d’objets. |
|
||||
| `world/level/block/CrafterBlock`, `world/level/block/entity/CrafterBlockEntity`, `world/level/block/ShelfBlock`, `world/level/block/CopperGolemStatueBlock` | Fabrication, mesure des cases, étagères et sélecteur par pose. |
|
||||
| `world/level/block/CreakingHeartBlock`, `world/level/block/entity/CreakingHeartBlockEntity`, `world/entity/animal/golem/CopperGolemAi`, `world/entity/ai/behavior/TransportItemsBetweenContainers` | Mesures et acteurs des montages. |
|
||||
| `world/level/block/TrappedChestBlock`, `world/level/block/entity/ChestBlockEntity`, `world/entity/animal/golem/CopperGolemAi` | Signal d’ouverture par joueurs et golems. |
|
||||
| `server/commands/ParticleCommand`, `server/commands/ComputeCommand`, `world/level/block/CommandBlock` | Différence entre fonctions administrateur et appareils accessibles en survie. |
|
||||
|
||||
Les preuves Sanctuary et historiques se trouvent dans
|
||||
`mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java`,
|
||||
`expansion/ExpansionRuntime.java`, le [cahier du langage](langage-et-machines.md)
|
||||
et la [lecture de Redstone Computer](redstone-computer-reference.md).
|
||||
L’ancien particuleur est dans le dépôt voisin `26.2/redstoner`, sous
|
||||
`src/main/java/fr/koka99cab/sanctuary26/redstoner/` : `block/ParticuleurBlock.java`,
|
||||
`block/entity/ParticuleurBlockEntity.java`, `particle/ParticuleurParticleTable.java`.
|
||||
Ces références ne constituent pas un portage ni une validation en 26.3.
|
||||
-1350
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,192 +0,0 @@
|
||||
# Chronologie de Sanctuary et calendrier en temps réel
|
||||
|
||||
**Repères donnés de mémoire : création le 16 mars 2026, développement le 24 mai,
|
||||
alpha le 17 août et bêta le 8 septembre.** Ils restent à confronter aux versions
|
||||
historiques ; un souvenir n’est pas une date de publication certifiée. Minecraft
|
||||
constitue la chronologie principale, dans laquelle Sanctuary ouvre sa propre histoire.
|
||||
Le temps du monde doit suivre le temps réel. Les conventions de calendrier
|
||||
ci-dessous constituent une proposition avant implémentation.
|
||||
|
||||
Ce document complète [l’histoire de Steve et de Galactium](histoire-steve-galactium.md)
|
||||
et les dossiers [2009–2014](recherche-minecraft-2009-2014.md),
|
||||
[2015–2026](recherche-minecraft-2015-2026.md) et
|
||||
[personnages](recherche-minecraft-personnages.md). « Chronologie principale »
|
||||
désigne l’histoire de Minecraft, pas une demande de fusion Git dans `main`.
|
||||
|
||||
## Les origines et les jalons
|
||||
|
||||
| Repère | Date ou origine | Sens |
|
||||
| --- | --- | --- |
|
||||
| **Ère Minecraft** | **17 mai 2009**, origine proposée | Anniversaire public reconnu par Mojang. Les prototypes antérieurs appartiennent à la préhistoire documentaire. |
|
||||
| **Ère Sanctuary** | **16 mars 2026**, souvenir de l’auteur | Origine de travail à confirmer avant de la figer dans les sauvegardes. |
|
||||
| **Développement / première version** | **24 mai 2026**, souvenir et mention de l’ancien site | 69 jours après cette origine ; annonce datée de mai, conservée dans Git en juillet seulement. |
|
||||
| **Alpha publique retrouvée** | **7 juillet 2026**, publication Modrinth et archive identique | Pack `2026.07.07`, Minecraft `26.1.2`, 113 jours après l’origine proposée. |
|
||||
| **Dépôt historique / jalon alpha évoqué** | **17 août 2026**, création Gitea attestée | 154 jours après l’origine proposée ; l’alpha publiée en juillet est antérieure. |
|
||||
| **Nouvelle base nommée Beta** | **8 septembre 2026**, création Gitea et premier commit attestés | 176 jours après l’origine proposée ; ses premières versions restent numérotées alpha. |
|
||||
| **Anniversaire officiel** | Jour de la future release officielle, à enregistrer lors de sa publication | Fête annuelle de Sanctuary, retenue par l’auteur ; ne pas la fixer à une ancienne alpha par défaut. |
|
||||
| **Âge de la partie** | Création effective de chaque monde serveur | Histoire locale de cette communauté : découvertes, charges, expansions et événements. |
|
||||
|
||||
Le 17 mai s’appuie sur l’anniversaire célébré par Mojang ; ce choix calendaire
|
||||
ne prétend pas dater le premier prototype. Le modèle humain antérieur au
|
||||
lancement public est étudié dans le dossier personnages.
|
||||
[Mojang, 17 mai 2024](https://www.minecraft.net/en-us/article/the-15th-anniversary-cape).
|
||||
|
||||
Un serveur créé en septembre ne prétend pas avoir fonctionné depuis mars.
|
||||
Il partage un passé narratif, puis possède son propre historique de partie.
|
||||
Les neuf disparus appartiennent à cette histoire proposée ; les réalisations
|
||||
des joueurs sont celles réellement enregistrées dans leur monde. Des serveurs
|
||||
séparés ne partagent pas leurs réserves de charges.
|
||||
|
||||
## Place dans la chronologie principale
|
||||
|
||||
Les possibilités du jeu sont documentées dans les dossiers historiques. Leur
|
||||
lecture narrative ci-dessous reste une proposition pour Sanctuary.
|
||||
|
||||
| Période | Repère Minecraft | Lecture mythologique proposée |
|
||||
| --- | --- | --- |
|
||||
| 2009–début 2010 | Premières phases et Indev ; construction, survie, fabrication, types de mondes dont les îles flottantes. | Steve mesure et rend habitable un lieu limité. |
|
||||
| 2010–2011 | Infdev, Alpha, Beta, 1.0 : élargissement du monde, redstone, Nether, mécanismes, puis End et enchantements. | Les limites et les relations entre les lieux deviennent des questions. |
|
||||
| 2012–2013 | 1.1 à 1.7.2 : commerce, écriture, automatisation et diversité des territoires. | Les solutions deviennent des installations capables de fonctionner pendant une absence. |
|
||||
| 2014 | 1.8 et arrivée d’Alex. | Une deuxième mémoire compare et conserve les transformations du monde. |
|
||||
| 2016–2019 | 1.9 à 1.15 : exploration, progrès, océans, métiers, vie des villages et abeilles. | Steve et Alex cherchent à relier des lieux et des usages sans en perdre l’histoire. |
|
||||
| 2020–2021 | 1.16 à 1.18 : Nether enrichi, repérage et retour, cuivre, améthyste, nouvelles profondeurs. | Matière, forme et adresse deviennent comparables : premières formulations de Galactium. |
|
||||
| 2022 | 1.19 et apparition commune des sept nouveaux skins. | Les neuf mettent en commun leurs expériences et retrouvent d’autres traces anciennes. |
|
||||
| 2023–2025 | 1.20, 1.21 et drops : archéologie, conservation des récits, fabrication automatique et nouveaux usages. | Les expériences deviennent une infrastructure ; la prospérité augmente les dépendances. |
|
||||
| **16 mars 2026, provisoire** | Création donnée de mémoire. | Origine proposée de la branche narrative Sanctuary. |
|
||||
| **24 mai 2026, à recouper** | Jalon de développement donné de mémoire. | Son équivalent dans la fiction reste à choisir. |
|
||||
| **7 juillet 2026** | Publication alpha `2026.07.07` sur Modrinth, archive retrouvée. | Première diffusion alpha directement recoupée par cet audit, pas nécessairement toute première diffusion du projet. |
|
||||
| **17 août 2026** | Création du dépôt historique attestée ; début exact de l’alpha 26.2 encore inconnu. | Une étape de développement ne date pas automatiquement une catastrophe du récit. |
|
||||
| **8 septembre 2026** | Nouvelle base `sanctuary-beta` attestée dans Git ; version initiale `0.1.0-alpha.1`. | Distinguer le nom du projet, sa phase souhaitée et les versions effectivement distribuées. |
|
||||
| Après la fondation | Minecraft et Sanctuary continuent d’évoluer ; chaque partie enregistre ses propres événements. | Les joueurs peuvent vivre l’histoire suivante, sans tout attribuer aux anciens. |
|
||||
|
||||
Si le 16 mars est confirmé, la dernière Java stable à la fondation est **1.21.11**, publiée le 9 décembre
|
||||
2025. **26.1 / Tiny Takeover** paraît le **24 mars 2026**, huit jours après la
|
||||
fondation, et avant le jalon éditorial du 24 mai retrouvé sur l’ancien site. **26.2 / Chaos Cubed**,
|
||||
du **16 juin 2026**, vient ensuite. Le détail de 26.3 et de ses préversions figure
|
||||
dans le dossier récent.
|
||||
[Java 1.21.11](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-11),
|
||||
[Java 26.1](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-1),
|
||||
[Java 26.2](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-2).
|
||||
|
||||
Cette comparaison ne prouve pas quelle version Minecraft le premier mod de
|
||||
développement utilisait. Elle n’oblige ni à recréer une ancienne instance ni
|
||||
à retirer les blocs récents des mondes actuels. Elle sert à dater un récit :
|
||||
un élément ajouté après la fin choisie pour l’activité des anciens demande une
|
||||
intervention ultérieure ou un prototype effectivement documenté. Disponibilité
|
||||
expérimentale et sortie stable sont deux événements différents.
|
||||
|
||||
Les sources locales et distantes, ainsi que leurs limites, sont consignées dans
|
||||
[l’audit des archives Sanctuary](recherche-archives-sanctuary.md). Les noms de
|
||||
versions historiques sont conservés tels quels : ils ne sont pas renommés pour
|
||||
les faire correspondre à ces quatre souvenirs.
|
||||
|
||||
La création ne date pas automatiquement la disparition des neuf. Même après
|
||||
confirmation des jalons, leur donner le sens d’une dernière période d’expériences
|
||||
ou d’une séparation serait un choix narratif supplémentaire.
|
||||
|
||||
## Compter les jours
|
||||
|
||||
Sous réserve de confirmer l’origine du 16 mars, proposition : afficher des
|
||||
**jours civils écoulés**, avec **J+0** le jour de
|
||||
l’origine. Les dates de fondation sont des dates, pas des instants UTC dont on
|
||||
aurait inventé l’heure. Elles restent affichées comme le 17 mai et le 16 mars,
|
||||
quel que soit le fuseau choisi pour présenter un événement.
|
||||
|
||||
Pour une même date civile `d` dans le fuseau calendaire de référence :
|
||||
|
||||
```text
|
||||
jour_minecraft(d) = différence de dates entre 2009-05-17 et d
|
||||
jour_sanctuary(d) = différence de dates entre 2026-03-16 et d
|
||||
jour_partie(d) = différence de dates entre la création de cette partie et d
|
||||
```
|
||||
|
||||
**jour_minecraft = jour_sanctuary + 6 147**.
|
||||
|
||||
| Date civile | Ère Minecraft | Ère Sanctuary proposée | Depuis le jalon du 24 mai |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 16 mars 2026 | J+6 147 | J+0 | Avant le jalon |
|
||||
| 24 mars 2026 | J+6 155 | J+8 | Avant le jalon |
|
||||
| 24 mai 2026 | J+6 216 | J+69 | J+0 |
|
||||
| 7 juillet 2026 | J+6 260 | J+113 | J+44 |
|
||||
| 17 août 2026 | J+6 301 | J+154 | J+85 |
|
||||
| 8 septembre 2026 | J+6 323 | J+176 | J+107 |
|
||||
| 11 septembre 2026 | J+6 326 | J+179 | J+110 |
|
||||
|
||||
Un affichage possible serait **« Sanctuary · J+179 · 08:00 »**, avec l’ère
|
||||
Minecraft et l’âge de la partie dans le calendrier détaillé. Le 11 septembre
|
||||
est un exemple daté, pas une valeur à figer dans le jeu. L’âge de la partie ne
|
||||
peut être rempli qu’à partir de son origine effective. Afficher « Jour 1 »
|
||||
plutôt que J+0 demanderait un décalage explicite ; 179 jours écoulés et le
|
||||
180e jour sont deux façons différentes de décrire la même date.
|
||||
|
||||
## Horloge commune et simulation
|
||||
|
||||
La direction retenue est une correspondance avec l’heure civile choisie par
|
||||
l’administrateur : à 8 h dans ce fuseau, il est 8 h dans le monde. Tous les
|
||||
joueurs partagent ce repère, même s’ils habitent dans des pays différents.
|
||||
Le sommeil ne fait pas sauter la nuit.
|
||||
|
||||
La date et l’heure ne dépendent pas de la vitesse de simulation. Les événements
|
||||
peuvent conserver un instant UTC, présenté dans le fuseau choisi. Les jours du
|
||||
calendrier changent à minuit dans son fuseau de référence, par différence de
|
||||
dates civiles : une journée de changement d’heure ne dure pas toujours
|
||||
86 400 secondes. Un fuseau géographique et un décalage horaire fixe sont deux
|
||||
choix différents à présenter clairement.
|
||||
|
||||
| Domaine | Comportement proposé |
|
||||
| --- | --- |
|
||||
| Date, heure et âge calendaire | Continuent avec le temps réel, même serveur arrêté. |
|
||||
| Ciel de l’Overworld | Retrouve l’heure civile courante à la reprise ; transitions et changements de fuseau restent à concevoir. |
|
||||
| Cultures, mobs, redstone et programmes | Ne reçoivent pas automatiquement des heures de simulation hors ligne. |
|
||||
| Charge libérée ou expansion acceptée | État persistant ; le passage du temps ne crée aucune charge ni nouvelle opération. |
|
||||
| Nether, End, Indoors et Backrooms | Peuvent partager une date commune en gardant leurs ambiances et cycles propres. |
|
||||
|
||||
Attendre quelques ticks permet de coordonner un montage ; attendre dimanche
|
||||
concerne une échéance civile. Le futur langage doit distinguer ces usages.
|
||||
Une sauvegarde ou restauration conserve l’origine de la partie. Changer le
|
||||
fuseau d’affichage ne réécrit pas les instants du journal. Changer le fuseau
|
||||
du calendrier ou corriger une horloge système exige une règle explicite.
|
||||
|
||||
## Événements, étoiles et mémoire
|
||||
|
||||
**Sanctuary aura un anniversaire annuel à la date de sa release officielle.**
|
||||
La direction est retenue ; le jour reste inconnu jusqu’à cette publication.
|
||||
L’historique retrouvé du 7 juillet concerne une alpha et ne remplace pas ce
|
||||
futur jalon. L’anniversaire commun du jeu peut être célébré par chaque serveur,
|
||||
en plus de l’âge propre de sa partie. Le calendrier le signalera ; la fête,
|
||||
ses activités et ses éventuelles récompenses restent à concevoir. Aucune
|
||||
récompense économique ni charge d’expansion automatique n’est décidée ici.
|
||||
|
||||
Loterie du dimanche, achat hebdomadaire des navets, offres horaires, anniversaires
|
||||
et panneaux d’événements s’appuieraient sur le même calendrier. Le traitement
|
||||
d’une échéance manquée pendant un arrêt reste à définir ; aucun tirage ni gain
|
||||
rétroactif automatique n’est décidé ici.
|
||||
|
||||
Le dimanche civil peut donner un repère concret au cycle de six jours puis un
|
||||
jour de fête. Un cycle compté depuis la fondation serait une autre convention,
|
||||
à distinguer de la semaine civile.
|
||||
|
||||
Les étoiles naissent toujours des accomplissements réels de la partie. Des
|
||||
milliers de jours d’histoire préalable ne donnent pas automatiquement les
|
||||
étoiles des anciens aux nouveaux joueurs. Des cartes célestes retrouvées dans
|
||||
l’observatoire pourraient témoigner d’un autre ciel : une piste narrative.
|
||||
|
||||
Un noyau brisé, une charge libérée, une expansion acceptée et un continent
|
||||
ouvert constituent des événements différents. Leurs dates suivent ce qui s’est
|
||||
effectivement produit sur le serveur. Le calendrier rend leur succession
|
||||
consultable sans fabriquer un historique d’activités antérieures.
|
||||
|
||||
## Points restant à définir
|
||||
|
||||
- Recouper les quatre dates données de mémoire ; fixer ensuite l’origine Sanctuary, l’origine Minecraft proposée du 17 mai 2009 et l’affichage J+0.
|
||||
- Enregistrer la date effective de la release officielle pour l’anniversaire annuel de Sanctuary.
|
||||
- Choisir le fuseau initial et les règles de changement de fuseau ou d’horloge.
|
||||
- Déterminer l’origine d’une ancienne partie sans date fiable, avec un contrat de migration avant toute écriture dans sa sauvegarde.
|
||||
- Définir reprises, expirations et opérations hors ligne pour les activités qui emploient le temps réel.
|
||||
- Choisir les éventuels événements de fiction correspondant aux jalons historiques, ainsi que la date ou l’intervalle de disparition des anciens.
|
||||
|
||||
Les relevés quotidiens du Blocodex décrits actuellement sont agrégés en UTC.
|
||||
Ils ne fournissent pas à eux seuls un journal local exhaustif ni un passé
|
||||
antérieur à leur installation. Le calendrier futur doit conserver ces limites.
|
||||
Ce document ne change ni l’horloge du jeu, ni les sauvegardes, ni les règles
|
||||
économiques d’une instance.
|
||||
@@ -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,507 +0,0 @@
|
||||
# La mémoire de Steve et la naissance de Galactium
|
||||
|
||||
Sanctuary peut raconter l’histoire d’une civilisation qui a grandi avec les
|
||||
possibilités de Minecraft. Les mises à jour fournissent l’ordre des découvertes,
|
||||
les personnages fournissent des regards différents sur ces découvertes, et les
|
||||
ruines permettent aux joueurs d’en retrouver les conséquences.
|
||||
|
||||
La chronologie documentaire et le récit ont deux statuts distincts. Les dates
|
||||
et contenus sont établis dans les dossiers sourcés ci-dessous. La biographie,
|
||||
les relations entre les anciens et l’explication mathémagique sont une
|
||||
**proposition de mythologie Sanctuary**, pas un récit officiel de Mojang.
|
||||
|
||||
## Corpus historique et périmètre
|
||||
|
||||
Le dossier suit Java Edition, des premières versions de 2009 aux contenus
|
||||
documentés en 2026. Il couvre les mises à jour de contenu et les premières
|
||||
apparitions importantes dans les versions de développement. Les correctifs
|
||||
strictement techniques ne reçoivent pas chacun un épisode mythologique. Les
|
||||
différences de disponibilité des personnages entre Java, Launcher et Bedrock
|
||||
sont signalées dans leur dossier.
|
||||
|
||||
| Dossier | Contenu |
|
||||
| --- | --- |
|
||||
| [Minecraft de 2009 à 2014](recherche-minecraft-2009-2014.md) | Premières phases, Alpha, Beta et versions 1.0 à 1.8 ; fondations du monde, des machines et des déplacements. |
|
||||
| [Minecraft de 2015 à 2026](recherche-minecraft-2015-2026.md) | Mises à jour de contenu modernes et snapshots déterminants ; statut des versions récentes. |
|
||||
| [Apparition des personnages](recherche-minecraft-personnages.md) | Steve, Alex et les sept nouveaux skins, dates et limites de ce que les sources établissent. |
|
||||
| [Chronologie Sanctuary et temps réel](chronologie-sanctuary.md) | Articulation avec Minecraft, repères de création et calendrier partagé. |
|
||||
| [Archives historiques Sanctuary](recherche-archives-sanctuary.md) | Recoupement des souvenirs de l’auteur avec les dépôts, annonces et anciens packs. |
|
||||
|
||||
Le Minecraft Wiki actuel est communautaire. L’ancien statut officiel a pris fin
|
||||
en 2021 ; les annonces Mojang servent de sources primaires complémentaires.
|
||||
L’accès direct à certaines pages anglaises du wiki étant indisponible, les
|
||||
dossiers distinguent les éléments recoupés et les limites des extraits indexés.
|
||||
La recherche est arrêtée au 11 septembre 2026 ; une préversion observée ne vaut
|
||||
pas confirmation de sa sortie stable. [S1](#sources-de-cadrage)
|
||||
|
||||
Les films, romans, Minecraft Dungeons, Legends et Story Mode ne définissent pas
|
||||
ici la biographie des neuf. Leurs continuités ne sont pas fusionnées avec celle
|
||||
du jeu Java. L’apparition d’une mécanique dans une mise à jour indique quand elle
|
||||
devient disponible dans le jeu ; elle ne prouve pas que Steve l’a inventée ni
|
||||
que ses habitants viennent de naître.
|
||||
|
||||
## Ordre des anciens
|
||||
|
||||
Le récit respecte trois arrivées documentaires : **Steve, puis Alex, puis Ari,
|
||||
Efe, Kai, Makena, Noor, Sunny et Zuri comme une même génération d’apparition**.
|
||||
La liste des sept n’est pas un ordre d’ancienneté. Leur annonce commune a lieu
|
||||
en 2022 ; aucune arrivée successive propre à chacun n’est établie par cette
|
||||
annonce. L’orthographe retenue pour Sanctuary est désormais **Efe**.
|
||||
[S2](#sources-de-cadrage) [S3](#sources-de-cadrage)
|
||||
|
||||
Cela suggère un récit avec un premier témoin, une deuxième mémoire indépendante,
|
||||
puis un groupe capable de transformer les expériences en infrastructure.
|
||||
Steve n’a pas besoin d’être le premier être vivant ni l’auteur de tous les
|
||||
vestiges. Son importance vient de la durée de son expérience. Alex ne vient pas
|
||||
simplement l’assister : Alex apporte une autre manière d’habiter et de raconter
|
||||
le monde. Les sept nouveaux personnages ont ensuite leurs propres recherches,
|
||||
leurs installations et leurs désaccords.
|
||||
|
||||
## Le fil mythologique proposé
|
||||
|
||||
### 1. Le monde que Steve peut encore compter
|
||||
|
||||
Les phases initiales donnent un vocabulaire limité de blocs et de constructions.
|
||||
Indev propose notamment des types de mondes insulaires et flottants, avant
|
||||
l’évolution vers Infdev. Ce précédent fournit à Sanctuary une parenté historique
|
||||
avec une forme ancienne de Minecraft. [S4](#sources-de-cadrage)
|
||||
|
||||
Dans le récit, Steve commence par mesurer ce qui l’entoure. Un mur a une
|
||||
épaisseur, une réserve un nombre de cases, un trajet une distance parcourable.
|
||||
Steve construit pour dormir, ranger, cultiver, éclairer. Le premier savoir est
|
||||
de rendre un petit endroit habitable et de pouvoir le retrouver.
|
||||
|
||||
La trace de cette époque serait modeste : une cabane agrandie plusieurs fois,
|
||||
des matériaux simples, un ancien niveau de sol encore visible sous une extension.
|
||||
Le cube originel pourrait être le repère à partir duquel Steve avait commencé
|
||||
à compter. Cela n’établit pas encore qui a créé ce cube.
|
||||
|
||||
### 2. L’horizon cesse de donner une limite
|
||||
|
||||
Infdev et l’évolution de la génération permettent de prendre comme tournant
|
||||
l’élargissement du monde. Dans la fiction, Steve découvre que marcher plus loin
|
||||
ne rapproche plus nécessairement d’un bord. L’abondance existe, mais elle se
|
||||
trouve ailleurs. Les besoins deviennent des problèmes de trajet, de retour et
|
||||
de transport.
|
||||
|
||||
Le Nether offre ensuite un autre rapport aux distances. L’enchantement et les
|
||||
premières rencontres avec l’End ouvrent la possibilité que les lieux obéissent
|
||||
à des relations que la simple marche ne révèle pas. Ces apparitions restent
|
||||
dans leur ordre historique, détaillé dans le dossier ancien.
|
||||
|
||||
Steve commence à conserver des signes. Certains se répètent autour d’objets
|
||||
ou de passages. La compréhension vient de plusieurs observations concordantes,
|
||||
pas d’un livre donnant immédiatement les règles de l’univers. Les glyphes des
|
||||
enchantements sont un point de départ visuel ; leur sens opératoire appartient
|
||||
à la fiction de Sanctuary.
|
||||
|
||||
### 3. Une installation continue après le départ de sa main
|
||||
|
||||
La redstone, les pistons, les systèmes de stockage et les hoppers marquent des
|
||||
étapes différentes de l’histoire réelle. Le récit peut faire grandir les
|
||||
constructions en respectant leur ordre, sans placer un dispositif tardif dans
|
||||
une ruine primitive dépourvue de réparations.
|
||||
|
||||
Steve résout d’abord un travail répétitif. L’eau arrive au bon endroit ; une
|
||||
porte attend une condition ; les ressources se rassemblent. Le progrès lui
|
||||
donne du temps pour partir. Les premières infrastructures sont des solutions
|
||||
utiles à des besoins ordinaires, dont on peut encore comprendre le fonctionnement.
|
||||
|
||||
Un ancien poste de pompage doit montrer ce qu’il alimentait. Un tunnel doit
|
||||
permettre de deviner ce qu’il rapprochait. Les premières traces de Galactium
|
||||
peuvent ainsi se cacher dans un dispositif utile, avant d’être identifiées comme
|
||||
une théorie générale.
|
||||
|
||||
### 4. Alex conserve ce que Steve laisse fonctionner
|
||||
|
||||
L’arrivée d’Alex appartient à la période 1.8, en 2014. Elle constitue un repère
|
||||
documentaire ; le rôle suivant est une proposition de personnage.
|
||||
[S3](#sources-de-cadrage)
|
||||
|
||||
Alex compare les lieux. Une carte, une coupe de terrain et la mémoire d’un
|
||||
trajet montrent que deux descriptions du même endroit peuvent diverger.
|
||||
Alex conserve les anciennes versions des plans au lieu de les remplacer.
|
||||
Des pages corrigées et plusieurs entrées d’un même bâtiment permettent de
|
||||
comprendre sa transformation.
|
||||
|
||||
Le désaccord fondateur pourrait être simple : Steve veut qu’une installation
|
||||
continue de servir ; Alex veut que quelqu’un puisse encore expliquer pourquoi
|
||||
elle existe et où elle mène. Ces deux ambitions sont utiles. Leur tension devient
|
||||
plus difficile à résoudre à mesure que les installations se multiplient.
|
||||
|
||||
### 5. Ils apprennent à donner une adresse à autre chose qu’un chemin
|
||||
|
||||
Les développements de l’End, des océans, des villages et des ressources donnent
|
||||
des années d’exploration et de construction au duo. Les développements ultérieurs
|
||||
des boussoles liées à la magnétite et des ancres de réapparition fournissent des
|
||||
motifs particulièrement adaptés à Galactium : un objet peut conserver une
|
||||
relation avec un lieu, et un retour peut demander une condition matérielle.
|
||||
[S5](#sources-de-cadrage) [S6](#sources-de-cadrage)
|
||||
|
||||
Dans le récit, Steve et Alex n’inventent pas tous ces phénomènes. Ils comparent
|
||||
les choses que le monde permet déjà. Leur découverte consiste à reconnaître
|
||||
une grammaire commune derrière plusieurs pratiques.
|
||||
|
||||
Un programme peut alors décrire une opération ; une construction en réunit les
|
||||
conditions. La formule devient opérante quand la matière, la forme et les accès
|
||||
s’accordent. C’est le début proposé de la mathémagie, plutôt qu’une permission
|
||||
arbitraire donnée à un personnage omnipotent.
|
||||
|
||||
### 6. Les profondeurs possèdent déjà leur histoire
|
||||
|
||||
Les nouvelles profondeurs, puis les cités anciennes, apportent un avertissement
|
||||
qui n’est pas nécessairement une menace. La présentation officielle du Deep Dark
|
||||
conserve elle-même une architecture et un passé énigmatiques. Elle ne permet pas
|
||||
d’attribuer toutes les cités aux neuf disparus. [S7](#sources-de-cadrage)
|
||||
|
||||
Dans Sanctuary, les deux personnages peuvent découvrir des installations dont
|
||||
la logique leur ressemble, sans reconnaître leur époque ni leurs auteurs.
|
||||
Certaines solutions ont peut-être été trouvées plusieurs fois. Ce soupçon donne
|
||||
une profondeur au monde sans obliger à révéler sa première civilisation.
|
||||
|
||||
Une boussole de récupération fournit aussi un motif réel : une information peut
|
||||
encore désigner une ancienne position après une mort. Le passage de cette idée à
|
||||
la recherche des espaces orphelins demeure notre invention. [S8](#sources-de-cadrage)
|
||||
|
||||
### 7. Sept personnes rendent la découverte habitable
|
||||
|
||||
À partir de leur apparition documentaire commune en 2022, les sept nouveaux
|
||||
personnages peuvent rejoindre le récit. Ils héritent des premières découvertes
|
||||
mais ne partagent pas tous la même idée de leur usage. L’archéologie, les moyens
|
||||
de conserver des livres et les développements de la fabrication automatique
|
||||
peuvent ensuite accompagner leurs travaux. [S9](#sources-de-cadrage)
|
||||
[S10](#sources-de-cadrage)
|
||||
|
||||
Galactium devient une infrastructure parce que le groupe résout de vrais
|
||||
problèmes : nourrir, transporter, construire à plusieurs, produire, retrouver
|
||||
les objets et agrandir les espaces devenus trop petits. La ville peut naître
|
||||
de ces besoins. Ses cuisines, bassins, ateliers et corridors précèdent leurs
|
||||
versions abandonnées.
|
||||
|
||||
Le succès produit la difficulté. Chaque dispositif rend un autre dispositif
|
||||
plus facile à construire. Les personnes qui comprennent toutes les dépendances
|
||||
deviennent moins nombreuses que les installations en service.
|
||||
|
||||
### 8. Le monde conserve les résultats, même quand leur usage se perd
|
||||
|
||||
Le lien proposé entre les systèmes est le suivant : un Indoor est un espace
|
||||
délimité dont on conserve la référence ; une Backroom garde une forme et de la
|
||||
matière lorsque leur contexte devient introuvable. Une expansion ajoute un
|
||||
territoire accessible. Les échanges et opérations peuvent laisser du ballast,
|
||||
dont les modalités économiques restent à concevoir.
|
||||
|
||||
Les noyaux rendent des expansions possibles. Leur casse les fait disparaître
|
||||
et libère une charge commune ; le serveur répond en galactique. Le noyau, la
|
||||
charge et le ballast ont des rôles distincts. Aucun passage du récit ne fixe
|
||||
une conversion quantitative entre eux ni ne remet le noyau dans un inventaire.
|
||||
|
||||
La grande question des anciens devient : comment cesser de faire fonctionner
|
||||
une installation sans retirer à quelqu’un son accès, son logement, son trajet
|
||||
ou son moyen de subsistance ? L’arrêt de chaque machine semble possible pris
|
||||
isolément. L’arrêt de l’ensemble ne possède plus de solution qu’ils sachent
|
||||
exécuter en préservant tous ces usages.
|
||||
|
||||
### 9. La séparation
|
||||
|
||||
**Proposition de dénouement à valider.** Les anciens entreprennent de réduire
|
||||
leur dépendance au système. Ils séparent les fonctions, déplacent des accès,
|
||||
isolent des volumes et cherchent à conserver une région habitable autour du
|
||||
repère initial. Des chantiers interrompus et des raccords provisoires témoignent
|
||||
de ce travail de maintenance.
|
||||
|
||||
Les territoires demeurent, mais leurs relations ne forment plus un monde que
|
||||
les anciens savent parcourir entièrement. Certaines destinations deviennent
|
||||
inaccessibles depuis les points de départ connus. Les neuf disparaissent des
|
||||
lieux que les joueurs explorent ; leur mort, leur transformation ou leur
|
||||
destination ne sont pas établies.
|
||||
|
||||
Sanctuary Island serait la région dont le repère commun tient encore. Les
|
||||
nouveaux joueurs y apparaissent dans la nature et retrouvent progressivement
|
||||
les installations. Une première expansion prouve qu’il est à nouveau possible
|
||||
d’établir une relation stable avec un autre territoire. Elle ne prouve pas que
|
||||
chaque nouveau continent existait déjà dans l’ancienne partie.
|
||||
|
||||
Le canon donné conserve son sens : ils ont construit quelque chose qu’ils ne
|
||||
savaient plus refermer. Le récit ne désigne pas un coupable unique. Les traces
|
||||
montrent des problèmes réellement résolus, puis des tentatives de préserver
|
||||
les bénéfices de ces solutions.
|
||||
|
||||
## Les neuf voix proposées
|
||||
|
||||
Ces rôles sont des personnages de Sanctuary à discuter. Ils ne découlent ni de
|
||||
la couleur des skins, ni de leur nom, ni d’une biographie officielle. Les sept
|
||||
personnages apparus en 2022 ne sont pas rétroactivement les auteurs des inventions
|
||||
antérieures ; ils peuvent les apprendre, les adapter et les critiquer.
|
||||
|
||||
**Précisions retenues le 11 septembre 2026 : Kai est le cuisinier fixe**, choisi
|
||||
par un tirage unique parmi les sept personnages ajoutés après Steve et Alex.
|
||||
Le livre de recettes lui est attribué. **Alex porte le domaine de la nature et
|
||||
des biomes ; Steve reste absent.** Les rôles d’**Ari pour le build**, de **Makena
|
||||
pour la redstone** et de **Zuri pour les étoiles et le temps** sont également
|
||||
retenus. Les questions personnelles et détails biographiques ci-dessous restent
|
||||
des propositions, distinctes de ces domaines désormais choisis.
|
||||
|
||||
| Personnage | Question personnelle | Contribution et trace à retrouver |
|
||||
| --- | --- | --- |
|
||||
| **Steve** | Comment faire durer ce que l’on a construit ? | Premiers montages, réparations successives, programmes simples toujours exécutés. Sa force est la continuité ; son angle mort serait de prendre un fonctionnement durable pour une compréhension durable. |
|
||||
| **Alex** | Comment connaître et habiter les milieux de Sanctuary ? | Domaine nature et biomes retenu ; plantes, observations et connaissance des lieux donnent des pistes d’activités. Cela ne réintroduit pas une recherche automatique de biomes. |
|
||||
| **Ari** | Quelle forme suffit pour qu’un intérieur tienne ? | Volumes d’essai, portes à différentes échelles, plans d’Indoors. Ari recherche la précision sans réduire l’habitat à un volume abstrait. |
|
||||
| **Efe** | D’où vient la matière, et que devient-elle après usage ? | Relevés, stations de prospection, réserves et premiers indices de ballast. Efe cherche à rendre visibles les conséquences de la production. |
|
||||
| **Kai** | Comment transformer une récolte en un repas réussi ? | Cuisinier fixe, livre de recettes à reconstituer, préparation et cuisson, concours et jugement des plats. Les règles de qualité restent à définir ; l’ancienne piste de liaisons spatiales est retirée de ce rôle. |
|
||||
| **Makena** | Comment une installation peut-elle servir plusieurs personnes ? | Ateliers, circulation des ressources, interfaces d’usage et programmes de Régie. Makena transforme une expérience en équipement collectif. |
|
||||
| **Noor** | Que reste-t-il quand une adresse ne répond plus ? | Journaux d’opérations, signaux sans destination et balises de recherche. Noor distingue une trace d’une preuve, y compris lorsque les autres veulent conclure. |
|
||||
| **Sunny** | Qu’est-ce qu’un espace habitable pour autre chose qu’une machine ? | Jardins, serres et chemins vivants restent des pistes à distinguer du domaine d’Alex. Le rôle de cuisinier et son livre sont attribués à Kai. |
|
||||
| **Zuri** | Comment savoir que deux événements appartiennent à la même histoire ? | Observatoire, relevés du ciel et correspondances avec les événements collectifs. Zuri cherche des repères de temps quand les espaces cessent d’en fournir. |
|
||||
|
||||
Leurs relations peuvent traverser leurs spécialités. Une archive de Noor peut
|
||||
contredire une carte d’Alex ; Sunny peut réutiliser une expérience d’Ari ; un
|
||||
montage de Steve peut être compris grâce aux mesures d’Efe. Aucun personnage
|
||||
ne devient le propriétaire exclusif d’un programme nécessaire aux joueurs.
|
||||
|
||||
### Identités, apparences et transformations
|
||||
|
||||
**Direction culturelle retenue : une mythologie implicitement queer.** Sanctuary
|
||||
accueille la possibilité de se transformer et de se définir sans que l’apparence
|
||||
détermine l’identité ou le rôle. La formule « on est tous trans » exprime ici
|
||||
une métaphore de la transformation commune ; elle n’assigne pas une identité
|
||||
trans à chaque personne. Cette lecture appartient à Sanctuary, sans prétendre
|
||||
définir le canon de Mojang ni les identités de personnes réelles.
|
||||
|
||||
Dans cette conception, skins et modèles aux bras larges ou fins ne constituent
|
||||
pas des catégories de genre donnant des capacités, des métiers ou des destins
|
||||
différents. L’absence de genre mécanique ne signifie pas l’absence de genre
|
||||
vécu : chaque joueur reste libre de se définir. Une apparence ne suffit pas à
|
||||
déduire cette identité.
|
||||
|
||||
**Pistes de mise en scène, encore proposées :** les visiteurs transmettent des
|
||||
pratiques choisies, sans métiers assignés selon le genre ; les changements
|
||||
d’apparence sont ordinaires et n’exigent pas de justification dans le récit.
|
||||
Des traces de plusieurs avatars pourraient appartenir à un même auteur de
|
||||
construction ou de programme. Ce serait une possibilité à explorer dans les
|
||||
ruines, pas une nouvelle identité confirmée des anciens ni une explication
|
||||
acquise de leur disparition.
|
||||
|
||||
### Entrer dans la mythologie — accueil avant la première apparition
|
||||
|
||||
**Déroulement demandé : fiche personnage, courte cinématique d’initialisation,
|
||||
puis arrivée naturelle.** La fiche présente d’abord le skin, le pseudo et une
|
||||
bio que le joueur peut écrire. La séquence en alphabet galactique apparaît
|
||||
ensuite, dans la cinématique, avec l’initialisation du personnage et une
|
||||
représentation graphique de recherche d’un emplacement sûr, comme un ordinateur
|
||||
qui démarre. L’ancien scénario avant le profil est retiré. Sanctuary comme
|
||||
archive qui semble se mettre à jour seule reste une piste narrative sous-jacente,
|
||||
dont l’accueil n’explique ni le fonctionnement ni l’origine.
|
||||
La lecture queer reste implicite, sans demander de se déclarer trans ni de
|
||||
partager cette lecture. La mise en scène précise reste en discussion.
|
||||
Le [cahier d’accueil et des profils](accueil-et-profils.md) détaille ce parcours
|
||||
et le système d’amitiés natif demandé ; aucun écran ni cinématique n’est livré.
|
||||
|
||||
Le personnage entre dans une histoire commencée avant lui, avec la possibilité
|
||||
d’y laisser ses propres traces. Le lien aux anciens tient à cette qualité
|
||||
d’habitant et d’auteur de constructions, de programmes et de souvenirs. Il
|
||||
n’impose ni descendance, ni réincarnation d’un ancien, ni rôle d’élu au joueur.
|
||||
|
||||
**Ordre retenu ; gestes et représentation encore à dessiner :**
|
||||
|
||||
1. Montrer le skin actuel et le nom de jeu, puis proposer une bio et son
|
||||
réglage de visibilité. Elle peut rester vide, parler de goûts Minecraft
|
||||
ou présenter un personnage fictif ; aucune classe n’est imposée.
|
||||
2. Après la fiche, jouer une courte séquence d’initialisation avec l’alphabet
|
||||
galactique et une recherche d’emplacement représentée graphiquement.
|
||||
Les motifs, animations et liens aux données réelles restent à concevoir ;
|
||||
aucun jeu de paramètres fictifs de sécurité n’est arrêté. Le serveur choisit
|
||||
et vérifie l’emplacement réel d’arrivée.
|
||||
3. Donner la main au joueur dans un endroit naturel. Les vestiges, machines
|
||||
et secrets se découvrent ensuite par l’exploration.
|
||||
|
||||
La première apparition effective sur le serveur, après cette initialisation,
|
||||
publie une seule fois **« helloworld {pseudo} »**. Les connexions suivantes
|
||||
gardent **« {pseudo} joins the game »** ; le suivi repose sur l’UUID, avec le
|
||||
pseudo actuel à l’affichage.
|
||||
|
||||
La proposition privilégie la première arrivée dans un monde. La bio peut être
|
||||
complétée plus tard ; durée de la cinématique, possibilité de la passer et
|
||||
traitement des connexions suivantes restent à préciser. Un changement de skin
|
||||
actualise l’apparence présentée et conserve
|
||||
la continuité de l’histoire du joueur. Il n’exige ni nouveau départ, ni annonce
|
||||
publique, ni justification narrative de cette transformation.
|
||||
|
||||
La scène n’infère aucun genre depuis le skin, le modèle de bras ou le pseudo.
|
||||
Aucune déclaration de genre, de transidentité ou de passé personnel n’est requise
|
||||
pour entrer. La bio facultative laisse la place à l’identité vécue
|
||||
du joueur ; elle ne décrète pas que son genre n’existe pas. Des formulations
|
||||
comme « Bienvenue » et « Entrer » permettent de s’adresser à tout le monde.
|
||||
|
||||
**Piste visuelle ultérieure :** représenter les joueurs avec le même soin que
|
||||
les anciens dans les portraits, cartes ou archives où leur présence a du sens.
|
||||
Ce parallèle peut se découvrir plus tard ; l’accueil ne dévoile pas d’emblée
|
||||
les neuf personnages ni leurs destins. Les archives d’apparences successives
|
||||
ne sont pas requises par cette proposition.
|
||||
|
||||
L’alphabet galactique appartient à la mise en scène d’initialisation, après le
|
||||
profil. Il ne constitue ni une explication de Galactium ni une nouvelle prise
|
||||
de parole du système. Les conseils, diagnostics, fragments biographiques ou
|
||||
appels automatiques déjà écartés ne sont pas réintroduits. La bio est écrite
|
||||
par le joueur ; la cinématique ne la complète pas ni ne révèle sa version
|
||||
réservée aux amis.
|
||||
|
||||
« Avant la connexion » désigne ici l’expérience **avant la première apparition
|
||||
jouable**. Le raccord à la connexion, aux données du monde et au chargement
|
||||
sera choisi dans le ticket d’interface ; aucune dimension d’attente ni nouvelle
|
||||
scène de spawn n’est introduite par ce document.
|
||||
|
||||
**Décision retenue : la présence dans la mythologie, l’histoire et la progression
|
||||
du personnage sont liées à l’UUID du compte du joueur**, dans le monde concerné.
|
||||
Le pseudo et le skin servent à présenter ce personnage et peuvent évoluer.
|
||||
La proposition de fonder cette continuité sur le nom est remplacée par ce choix.
|
||||
|
||||
Pour la future réalisation, le rattachement au compte authentifié utilise
|
||||
son **UUID**, tandis que l’accueil affiche le pseudo et le skin actuels.
|
||||
Changer de nom ou de skin ne doit pas créer un autre
|
||||
personnage, effacer sa progression ou réattribuer ses actes à quelqu’un qui
|
||||
reprendrait son ancien nom. L’écran n’affiche pas cet identifiant technique
|
||||
et n’exige pas de conserver publiquement la liste des anciens pseudos.
|
||||
Le contrat d’identité et son raccord au journal serveur restent à implémenter ;
|
||||
aucune migration de données n’est effectuée ici.
|
||||
|
||||
### Présences de passage
|
||||
|
||||
L’auteur souhaite que des personnages **visitent Sanctuary pendant une journée**,
|
||||
se promènent et proposent au clic droit un menu de discussion, des quêtes ou
|
||||
des échanges spécialisés. Kai peut organiser des concours de cuisine ; le
|
||||
personnage du build peut demander des constructions et incarner l’accès au
|
||||
catalogue galactique. Un marchand de décoration est une piste, sans identité
|
||||
fixée. L’invocation évoquée auparavant reste en réserve.
|
||||
|
||||
La nature de ces présences reste à écrire ; leur visite n’explique pas encore
|
||||
la disparition des anciens ni leur état actuel. Steve demeure absent ; des
|
||||
**flashs d’Herobrine** sont souhaités, sans identifier Herobrine à Steve ou en
|
||||
faire automatiquement un boss. Les rendez-vous de visiteurs se rattachent au
|
||||
temps réel et ne remplacent pas les communications restreintes de Galactium.
|
||||
Voir les [visiteurs et leurs pratiques](progression-et-integrations.md#visiteurs-et-pratiques-des-anciens).
|
||||
|
||||
## Mathémagie commune
|
||||
|
||||
**Direction retenue : Galactium est le vide qui structure ce qui l’entoure.**
|
||||
L’auteur souhaite une approche de science-fiction, dans un esprit d’exploration
|
||||
et d’ingénierie évoquant Star Trek, avec une ampleur qui peut sembler divine.
|
||||
Cette cosmologie appartient à Sanctuary. Galactium désigne désormais aussi le
|
||||
fondement du système d’expansion qui porte déjà ce nom.
|
||||
|
||||
Formulation proposée pour le récit :
|
||||
|
||||
> La matière donne une forme aux choses. Le Galactium leur permet de tenir ensemble.
|
||||
|
||||
Ce vide serait présent entre les îles comme dans les relations entre blocs,
|
||||
lieux et dimensions. Les anciens découvrent progressivement des effets
|
||||
reproductibles : maintenir un volume, conserver une adresse, relier deux lieux.
|
||||
Ils construisent des instruments, comparent leurs résultats et transmettent des
|
||||
programmes. Leur maîtrise pratique peut progresser plus vite que leur compréhension
|
||||
de ce qu’ils utilisent. Cette piste prolonge l’histoire des solutions devenues
|
||||
une infrastructure dont ils ne savent plus organiser l’arrêt.
|
||||
|
||||
La comparaison avec Dieu exprime sa portée cosmologique. Sa conscience, sa
|
||||
volonté et l’existence d’une intention restent ouvertes. La réponse galactique
|
||||
du serveur lors de la casse d’un noyau pourrait être comprise comme un signal
|
||||
ou une présence : ces lectures sont des propositions. Les ruines
|
||||
peuvent conserver des mesures, des essais et des dispositifs de secours ; leurs
|
||||
auteurs ont appris à agir sur le monde avec une connaissance incomplète.
|
||||
|
||||
Galactium peut ainsi se manifester comme une présence qui habite le serveur.
|
||||
Ses secrets retenus sont des coordonnées, des morceaux de programmes et des
|
||||
rendez-vous préécrits. Leur présentation passe par des [afficheurs diégétiques](langage-et-machines.md#afficheurs-diégétiques-textes-dynamiques-et-statistiques)
|
||||
qui peuvent afficher du texte dynamique et des statistiques. Les messages
|
||||
d’histoire, conseils, diagnostics et demandes mystérieuses proposés puis rejetés
|
||||
ne font pas partie de cette sélection.
|
||||
|
||||
L’omniprésence de Galactium ne donne pas un usage illimité de ses effets. Les
|
||||
programmes en assembleur pilotent les opérations d’installations construites ;
|
||||
les charges collectives, ressources et conditions de réalisation gardent leurs
|
||||
rôles. L’origine des Endermen et la cause précise de la disparition des anciens
|
||||
ne sont pas résolues par cette définition.
|
||||
|
||||
Une notation de conception peut guider l’écriture :
|
||||
|
||||
**Lieu = matière + forme + adresse + relations maintenues.**
|
||||
|
||||
C’est une règle de fiction, à transformer plus tard en contrats de jeu. Elle
|
||||
ne prétend pas être une loi physique ni une égalité de quantités de blocs.
|
||||
Les programmes permettent de mesurer, délimiter, transformer, relier et suivre
|
||||
des opérations. Les installations apportent les moyens matériels et les
|
||||
conditions d’exécution.
|
||||
|
||||
Le cube possède six orientations, autour d’un point de référence. Les sept
|
||||
boules pourraient se rattacher à cette image d’un repère complet. Leurs épreuves
|
||||
déjà envisagées — fortune, cauchemar, Notch et cristal du nécromancien — gardent
|
||||
leurs intentions. Les fonctions restantes et la signification finale restent
|
||||
à définir ; neuf personnages ne doivent pas être artificiellement ramenés à
|
||||
sept postes fixes.
|
||||
|
||||
Les Cavernes restent un monde de minage dont le donjon débloque les portails
|
||||
collectivement. Le Nether et l’End gardent leurs accès propres. Les fonctions
|
||||
thématiques proposées dans le [cahier des machines](langage-et-machines.md#programmes-à-thème-et-fonctions-des-lieux)
|
||||
peuvent enrichir cette histoire sans imposer une campagne à étapes obligatoires.
|
||||
Le monde Alpha peut conserver une expérience primitive, avec ses formes et ses
|
||||
textures ; sa relation à Notch demeure un choix de fiction Sanctuary distinct
|
||||
de l’histoire réelle de son développeur.
|
||||
|
||||
## Rendre cette histoire visible dans les structures
|
||||
|
||||
Une ruine doit pouvoir être comprise avant d’être lue. Elle possède une
|
||||
ressource d’entrée, une transformation ou une circulation, un résultat et des
|
||||
personnes auxquelles elle servait. Son abandon révèle des usages interrompus.
|
||||
|
||||
| Trace | Lecture possible en jeu |
|
||||
| --- | --- |
|
||||
| Deux générations de matériaux et un ancien seuil conservé | Le bâtiment a été agrandi ; sa date de fondation et sa dernière intervention diffèrent. |
|
||||
| Une conduite encore raccordée à un bassin, avec une commande manquante | L’installation fournissait de l’eau ; le défaut est compréhensible et réparable. |
|
||||
| Un programme sur disquette, ses variantes et un montage d’essai | Les anciens comparaient des solutions ; le code devient un outil transmissible. |
|
||||
| Une salle d’expansion déséquipée, avec plans et raccords | La fonction peut être reconstruite ailleurs ; l’endroit conserve sa valeur de découverte. |
|
||||
| Un journal de transfert et une destination qui ne répond plus | La perte est un lien à examiner, pas la preuve immédiate d’une mort. |
|
||||
| Des palettes de Backrooms associées à différentes activités anciennes | L’histoire économique laisse des couches que les joueurs peuvent interpréter. |
|
||||
|
||||
La sélection actuelle reste un départ naturel, l’observatoire, la salle
|
||||
d’expansion, un atelier caché, la traversée souterraine et le donjon majeur.
|
||||
Les Lost Cities sont principalement destinées aux continents d’expansion ;
|
||||
les autres bâtiments restent en réserve sur l’île initiale. Les salles secrètes
|
||||
y transmettent des solutions utilisables et reproductibles par leurs montages,
|
||||
avec terminaux, contrôleurs, afficheurs, disquettes et blocs vanilla. La grande
|
||||
salle ancienne reste essentielle, sans dépendre d’une ville au-dessus. Écrire ce
|
||||
passé et ces usages ne les active pas automatiquement dans la génération actuelle.
|
||||
|
||||
## Décisions à valider et limites
|
||||
|
||||
Le respect de l’ordre des mises à jour, l’orthographe Efe et la distinction entre
|
||||
faits et fiction sont établis pour ce dossier. Le récit de la séparation,
|
||||
les relations personnelles, la fonction précise du cube et les sept épreuves
|
||||
restent des propositions. La cause finale de la disparition des neuf doit être
|
||||
choisie consciemment, avec ce que les joueurs peuvent réellement en découvrir.
|
||||
|
||||
Le calendrier souhaité suit le temps réel et prend Minecraft comme chronologie
|
||||
principale. Les repères Sanctuary donnés de mémoire — 16 mars, 24 mai, 17 août
|
||||
et 8 septembre 2026 — font l’objet d’un audit avant d’être figés. Leur sens
|
||||
fictionnel, l’âge biologique des personnages et la date de leur disparition
|
||||
restent à choisir. Une snapshot peut inspirer un prototype abandonné ; une fonctionnalité
|
||||
retirée ne devient pas pour autant un pouvoir disponible dans le mod.
|
||||
|
||||
Ce dossier n’implémente aucune dimension, machine, quête ou altération de monde.
|
||||
Les espaces orphelins demeurent une fiction correctement sauvegardée ; la
|
||||
recherche des objets perdus doit conserver leur unicité. La mythologie prépare
|
||||
les futurs tickets jouables et leurs critères de vérification.
|
||||
|
||||
## Sources de cadrage
|
||||
|
||||
Les dossiers historiques contiennent les références détaillées par mise à jour.
|
||||
Les sources suivantes soutiennent les rapprochements factuels employés ici ;
|
||||
elles ne valident pas les biographies et la cosmologie proposées.
|
||||
|
||||
- **S1. Minecraft Wiki**, [Community portal — Microsoft status update](https://minecraft.wiki/w/Minecraft_Wiki:Community_portal/Microsoft_status_update), avis sur la fin du statut officiel, 2021.
|
||||
- **S2. Mojang, Sofia Dankis**, [Introducing New Default Minecraft Skins](https://www.minecraft.net/en-us/article/introducing-new-default-skins), 20 octobre 2022 ; [Minecraft Live 2022: The Recap](https://www.minecraft.net/en-us/article/minecraft-live-2022-the-recap), annonce commune.
|
||||
- **S3. Minecraft Wiki**, [Alex](https://minecraft.wiki/w/Alex), historique Java 1.8-pre1 ; voir les dates et leurs recoupements dans le dossier personnages.
|
||||
- **S4. Minecraft Wiki**, [Seed (world generation)](https://minecraft.wiki/w/Seed_%28world_generation%29), historique Indev des types flottants ; [World type, édition japonaise](https://ja.minecraft.wiki/w/ワールドタイプ), évolution des types de mondes.
|
||||
- **S5. Mojang, Duncan Geere**, [Block of the Week: Lodestone](https://www.minecraft.net/nb-no/article/block-week--lodestone), 20 août 2020.
|
||||
- **S6. Mojang, Adrian Östergård**, [Minecraft Snapshot 20w12a](https://www.minecraft.net/da-dk/article/minecraft-snapshot-20w12a), 18 mars 2020, ancre de réapparition.
|
||||
- **S7. Mojang**, [Around the Block: Deep Dark](https://www.minecraft.net/en-us/article/around-block--deep-dark), présentation du biome et commentaire de conception de Mariana Salimena.
|
||||
- **S8. Mojang, Duncan Geere**, [Taking Inventory: Recovery Compass](https://www.minecraft.net/de-de/article/taking-inventory--recovery-compass), 19 janvier 2023.
|
||||
- **S9. Mojang, Sofia Dankis**, [The Trails & Tales Update is Here](https://www.minecraft.net/en-us/article/trails-tales-update-here), 7 juin 2023.
|
||||
- **S10. Mojang, Duncan Geere**, [Crafting with the Crafter](https://www.minecraft.net/fr-ca/article/crafting-crafter), 6 juin 2024.
|
||||
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,105 +0,0 @@
|
||||
# Sanctuary — journal des transformations et progression du monde
|
||||
|
||||
**Conception en discussion, WG-26.** L’auteur demande un suivi profond des
|
||||
joueurs, des entrées, des sorties et des transformations dans le temps. Ce
|
||||
document prépare un futur journal ; il ne crée aucune collecte, génération,
|
||||
récompense ou migration de sauvegarde.
|
||||
|
||||
## Intention
|
||||
|
||||
Le serveur doit pouvoir relier les découvertes et la production à leurs effets :
|
||||
matériaux obtenus, procédés utilisés, programmes exécutés, objets échangés ou
|
||||
perdus et étapes accomplies. Cette histoire nourrit les propositions de plans,
|
||||
les Backrooms, les Indoors et les futurs donjons. L’objectif ressemble à un
|
||||
suivi expérimental : savoir ce qui est entré dans une opération, ce qui en est
|
||||
sorti et comment elle s’inscrit dans une suite d’actions.
|
||||
|
||||
## Ce qui existe et ce qui manque
|
||||
|
||||
Lecture du dépôt de développement le **11 septembre 2026** : le Blocodex possède
|
||||
une mémoire personnelle des blocs ; les recensements de stocks sont partiels.
|
||||
L’historique d’activité conserve cinq compteurs journaliers UTC pour l’ensemble
|
||||
du serveur : blocs minés et posés, objets ramassés, jetés et fabriqués. Il ne
|
||||
reconstitue pas les consommations ni les transformations des machines.
|
||||
|
||||
Références de cette lecture, dans le dépôt voisin `sanctuary-beta` :
|
||||
`docs/blocodex.md`, `docs/cosmologie.md` et
|
||||
`mods/sanctuary/src/main/java/fr/koka/sanctuary/knowledge/history/MaterialActivityHistory.java`.
|
||||
Ces compteurs ne donnent pas l’auteur de chaque opération, les entrées d’une
|
||||
recette, les destinations ou les pertes. Ils ne constituent pas le journal
|
||||
transactionnel nécessaire aux récompenses ou aux restitutions. Les événements
|
||||
antérieurs ou non observés restent inconnus ; ils ne sont pas reconstruits
|
||||
artificiellement à partir d’un total.
|
||||
|
||||
## Décrire des opérations réellement terminées
|
||||
|
||||
Le format reste à définir. Les informations proposées sont : identifiant stable
|
||||
d’opération, date UTC et ordre serveur, joueur ou machine à l’origine de l’action,
|
||||
type d’opération, lieu et références utiles, entrées et sorties effectives, recette
|
||||
ou programme concerné et version pertinente. Une opération automatique ne doit
|
||||
pas être attribuée au joueur le plus proche : opérateur, propriétaire et
|
||||
bénéficiaire peuvent être différents.
|
||||
|
||||
| Fait | Sens à préserver |
|
||||
| --- | --- |
|
||||
| Observation | Preuve personnelle qu’un bloc a été aperçu ; ne suffit pas au déclencheur de plan fondé sur des blocs obtenus. |
|
||||
| Obtention | Possession attestée pouvant proposer un plan associé ; ne signifie pas qu’un stock suffisant est encore disponible. |
|
||||
| Production | Produits réellement créés par une récolte, une extraction ou un procédé ; un compteur de blocs minés ne donne pas toujours les drops obtenus. |
|
||||
| Transformation | Entrées consommées, sorties, restes et coproduits d’une même opération, avec sa recette ou son procédé. |
|
||||
| Transfert | Déplacement entre inventaires, machine, sol ou joueur ; déplacer puis ramasser une stack ne la produit pas une seconde fois. |
|
||||
| Consommation | Usage final, par exemple manger un aliment ; distinguer ce cas de son emploi comme ingrédient. |
|
||||
| Perte confirmée | Destruction, combustion ou disparition dans le vide selon les règles à instrumenter. Mort, déconnexion et chunk déchargé ne prouvent pas une destruction. |
|
||||
| Récompense ou restitution | Attribution distincte, référencée une seule fois. Une récupération en Backrooms doit se rattacher à la perte concernée sans laisser également l’original récupérable. |
|
||||
|
||||
Exemple proposé pour une future chaîne d’It’s Alive ! : récolte de blé, passage
|
||||
au moulin pour obtenir de la farine, préparation et cuisson du pain, puis repas.
|
||||
Les quantités, ingrédients supplémentaires, combustible et restes proviennent
|
||||
des recettes effectivement exécutées. Chaque transfert au coffre reste un
|
||||
transfert ; il ne gonfle pas la production. Cet exemple n’atteste pas une
|
||||
recette actuellement livrée.
|
||||
|
||||
Une variation de stock seule n’explique pas sa cause. L’instrumentation doit
|
||||
observer les opérations à leur aboutissement et signaler les zones non couvertes.
|
||||
Pour les programmes, le suivi vise leurs actions sur les appareils et la matière ;
|
||||
le journal d’économie n’a pas besoin de recopier chaque instruction CPU exécutée.
|
||||
|
||||
## Ce que l’histoire pourra produire
|
||||
|
||||
| Usage futur | Données et résultat à concevoir |
|
||||
| --- | --- |
|
||||
| Bibliothèque de plans | Associer les blocs obtenus et familles de matériaux à des architectures pertinentes. Distinguer proposition, déblocage et matériaux nécessaires à la construction. |
|
||||
| Backrooms | Réutiliser des traces de l’activité pour composer des espaces et des distributions ; relier séparément les objets réellement perdus à leur éventuelle récupération. |
|
||||
| Indoors procéduraux | Composer des espaces contrôlés et intéressants à partir de thèmes ou usages observés. Propriété, accès et génération gardent leur contrat propre. |
|
||||
| Donjons et raids | Proposer des épreuves correspondant aux étapes atteintes, avec de nouvelles combinaisons de salles, ennemis, objectifs et coordination. La progression ne se résume pas à augmenter les points de vie. |
|
||||
|
||||
Le ballast conserve son rôle cosmologique de trace des opérations ; une unité
|
||||
comptée ne devient pas automatiquement une unité de ballast ni un objet disponible
|
||||
dans une Backroom. Les conversions, règles de composition et ressources restent
|
||||
à définir. Les fonctions reproductibles des machines et l’autonomie d’It’s Alive !
|
||||
sont conservées.
|
||||
|
||||
Chaque nouvelle génération doit pouvoir être reliée à une période d’observation,
|
||||
sa couverture, la règle de sélection, la graine et la version du générateur.
|
||||
Ces paramètres sont fixés pour la génération concernée : l’évolution du serveur
|
||||
ne réécrit pas une installation déjà visitée et ne change pas une épreuve en cours.
|
||||
Un raid peut être conçu pour un effectif donné puis conserver cette difficulté
|
||||
pendant sa tentative. Les nouvelles versions de recettes ne réinterprètent pas
|
||||
les anciennes transformations comme si leurs rendements avaient toujours été identiques.
|
||||
|
||||
## Continuité et coût du suivi
|
||||
|
||||
L’ambition de couverture doit être rendue mesurable : quelles opérations sont
|
||||
instrumentées, depuis quand et avec quelles interruptions. Prévoir collecte
|
||||
événementielle, budgets d’écriture, files bornées et regroupements exploitables
|
||||
sans scanner le monde ni charger des chunks pour tenter de reconstruire le passé.
|
||||
La durée de conservation des événements détaillés et celle des agrégats restent
|
||||
à choisir ; un agrégat ne prétend pas conserver une causalité qu’il a perdue.
|
||||
|
||||
Le journal qui décide d’un achat, d’un quota de raid ou d’une restitution doit
|
||||
définir sa reprise après incident. Un même événement ne doit pas attribuer deux
|
||||
récompenses après reconnexion. Les agrégats actuels ne suffisent pas à garantir
|
||||
ce comportement. Aucun format existant n’est modifié par cette conception ;
|
||||
une implémentation demandera son propre contrat de sauvegarde et de migration.
|
||||
|
||||
Les applications et règles de raid sont précisées dans le
|
||||
[cahier de progression](progression-et-integrations.md#raids-collectifs-instanciés).
|
||||
@@ -1,788 +0,0 @@
|
||||
# Sanctuary — écriture galactique et machines programmables
|
||||
|
||||
**Conception en discussion, WG-26.** Ce cahier distingue la direction demandée
|
||||
des rôles proposés. Aucun langage, bloc ou changement de progression n’est
|
||||
implémenté par ce document.
|
||||
|
||||
## Direction retenue
|
||||
|
||||
Exploiter l’alphabet galactique de Minecraft comme écriture d’un langage secret
|
||||
dans Sanctuary. Ce langage doit relier la redstone programmable, les expansions,
|
||||
les waystones et l’End. **Le socle informatique retient trois blocs :
|
||||
contrôleur, terminal et afficheur, avec les disquettes comme objets. Le bloc Assembleur
|
||||
est retiré de la conception.** L’assemblage du code est une fonction du terminal.
|
||||
|
||||
**Précisions retenues : programmation directement en assembleur, six faces
|
||||
d’entrée/sortie et une véritable RAM adressable pour les contrôleurs.** L’auteur
|
||||
n’a pas de préférence arrêtée sur le terme « sérialisable ». La base de travail
|
||||
proposée est de sauvegarder l’état complet du contrôleur ; une communication
|
||||
en série plus riche reste une extension à étudier selon les besoins des montages.
|
||||
Une première proposition détaillée définit maintenant le langage et
|
||||
l’architecture ; elle reste à valider et à implémenter.
|
||||
|
||||
La [lecture du Redstone Computer historique](redstone-computer-reference.md)
|
||||
retrouve déjà un assembleur, de la RAM, six faces, des bus, du tri d’items et un
|
||||
réseau. Son CPU repart cependant au début à chaque tick. Le cahier de référence
|
||||
distingue les capacités à reprendre d’une future machine réellement continue,
|
||||
ainsi que les limites matérielles d’un objet très puissant utilisable en survie.
|
||||
|
||||
L’[audit redstone 26.3](audit-redstone-26.3.md) inventorie les capteurs,
|
||||
comparateurs, actionneurs, transports et automatismes vanilla. Il situe ce
|
||||
que les ordinateurs rendent plus compact et ce que les appareils Sanctuary
|
||||
ajoutent réellement. Il sépare signal électrique, transfert physique d’objets
|
||||
et échange de données avec un périphérique, notamment pour le particuleur.
|
||||
|
||||
Le [dossier Redstone Language V0.1](redstone-language-extensions.md) complète
|
||||
ce cadrage avec les [instructions](redstone-language-instructions.md), les
|
||||
[fiches des appareils](redstone-language-composants.md) et les
|
||||
[composants de signal et de transport](redstone-language-signaux-et-transport.md).
|
||||
**Le socle minimal n’est pas un plafond : un manque peut révéler un nouveau
|
||||
bloc à créer.** Le condensateur et ses interactions sont désormais retenus.
|
||||
Le convoyeur se pose comme un rail, à vitesse unique, avec arrêt sans redstone ;
|
||||
la commande d’inversion reste à choisir. Lecteur de stock, aiguilleur, convertisseur, capteurs et
|
||||
connexion adressée restent proposés. Transistor, atténuateur et impulseur
|
||||
sont écartés ; une [exploration par objets et cristaux](redstone-language-objets-et-cristaux.md)
|
||||
cherche d’autres gestes de machine. Les valeurs V0.1 restent un profil d’essai,
|
||||
pas du code livré.
|
||||
|
||||
Le [cahier des machines multiblocs](machines-multiblocs.md) explore les appareils
|
||||
que ces programmes pourront coordonner : grand four (nom proposé : Fourneau),
|
||||
Fût, grand baril de fermentation, Trémie, Carillon, méga-pistons et autres
|
||||
constructions. Les collections visibles et le Métablit y sont aussi étudiés.
|
||||
Chaque appareil conserve ses gestes locaux, son contenu et ses
|
||||
recettes ; la programmation n’est pas nécessaire à son premier usage manuel.
|
||||
|
||||
Nous étendons le lore à partir de motifs de Minecraft. Le lien opératoire entre
|
||||
ces glyphes, les destinations et les machines est une création Sanctuary.
|
||||
L’origine du langage, son rapport exact à l’End et ce que les neuf disparus ont
|
||||
découvert restent à écrire.
|
||||
|
||||
Le cadrage cosmologique retient **Galactium comme le vide structurant qui permet
|
||||
aux choses d’avoir une place et des relations**. Ce nom englobe le phénomène et
|
||||
le système d’expansion développé pour en utiliser certains effets. L’approche
|
||||
privilégie observation, expérimentation et ingénierie ; conscience ou volonté
|
||||
éventuelles restent ouvertes. Voir la [mathémagie proposée](histoire-steve-galactium.md#mathémagie-commune).
|
||||
Les programmes restent de l’assembleur exécuté par les contrôleurs : ils calculent
|
||||
et pilotent les opérations des installations. Une opération spatiale exige
|
||||
toujours ses conditions matérielles et collectives ; cette cosmologie n’ajoute
|
||||
ni instruction universelle ni ressource infinie utilisable par un joueur.
|
||||
|
||||
Le jeu de référence 26.3-pre-2 emploie la police `minecraft:alt` pour les mots
|
||||
tirés au hasard par `EnchantmentNames`, affichés dans `EnchantmentScreen`.
|
||||
Cette lecture du code vanilla fournit une référence visuelle ; la grammaire
|
||||
exécutable et ses effets sont à concevoir pour Sanctuary.
|
||||
|
||||
## Proposition : une écriture, des instructions et des usages
|
||||
|
||||
L’écriture donne les signes visibles sur les inscriptions et les appareils.
|
||||
La grammaire définit les opérations que les joueurs peuvent composer. Les
|
||||
installations donnent à ces opérations des effets dans le monde.
|
||||
|
||||
Un premier vocabulaire pourrait permettre de **lire, comparer, mémoriser,
|
||||
écrire une sortie, attendre et répéter**. Des usages spatiaux pourraient ensuite
|
||||
introduire des notions de **référence, liaison, destination et stabilité**.
|
||||
Leurs noms, glyphes, syntaxe et conditions d’apprentissage ne sont pas arrêtés.
|
||||
|
||||
La lecture des ruines peut apprendre à reconnaître un programme par ce qu’il
|
||||
faisait : pomper, temporiser une porte, aiguiller une voie, maintenir une liaison.
|
||||
Des variantes annotées et des montages encore utilisables peuvent permettre
|
||||
d’expérimenter. Le partage des programmes entre joueurs est retenu ; ses
|
||||
modalités de copie et de vente restent à définir.
|
||||
|
||||
**Le mode de programmation retenu est l’assembleur direct.** Registres, adresses,
|
||||
instructions et étiquettes doivent permettre d’écrire un vrai programme. Une
|
||||
transcription saisissable au clavier et un affichage en glyphes peuvent présenter
|
||||
le même code ; les mnémotechniques exacts restent à choisir. L’édition visuelle
|
||||
n’est pas le mode retenu. Une aide à la lecture et au diagnostic est proposée ;
|
||||
sa disponibilité et une éventuelle traduction progressive restent à décider.
|
||||
Le Blocodex ne devient pas automatiquement un lexique de ce langage.
|
||||
|
||||
## Proposition de lien avec les Endermen
|
||||
|
||||
La téléportation des Endermen fournit un point d’appui observable dans Minecraft
|
||||
([présentation officielle](https://www.minecraft.net/en-us/article/minecraft-mobs)).
|
||||
Dans le lore proposé pour Sanctuary, certaines séquences galactiques pourraient
|
||||
décrire les opérations de l’espace que les Endermen accomplissent.
|
||||
|
||||
Le joueur pourrait découvrir une même séquence dans des traces de téléportation,
|
||||
sur une ancienne waystone et dans un programme d’atelier. Une répétition permet
|
||||
d’associer les signes à un effet, puis une annotation laissée par les anciens
|
||||
joueurs aide à comprendre son usage. Les noms galactiques des Endermen déjà
|
||||
évoqués dans la vision peuvent prolonger cette présence de l’écriture.
|
||||
|
||||
Piste narrative : les anciens joueurs ont appris à reproduire dans leurs
|
||||
machines certaines opérations observées chez les Endermen. Cette hypothèse
|
||||
reste à valider ; elle ne fixe ni l’inventeur du langage, ni l’identité des
|
||||
Endermen, ni la cause de la disparition des neuf personnages. Le déchiffrement
|
||||
des lettres et l’apprentissage des opérations sont deux étapes de compréhension.
|
||||
|
||||
Les premières instructions redstone pourraient s’apprendre avec un montage
|
||||
simple. Les notions de destination, liaison et stabilité se découvriraient par
|
||||
les installations spatiales. Les conditions de ces découvertes et les traces
|
||||
visuelles de téléportation sont des propositions, pas de nouveaux effets livrés.
|
||||
|
||||
## Ensemble minimal retenu
|
||||
|
||||
| Élément | Fonction | Précision proposée dans V0.1 |
|
||||
| --- | --- | --- |
|
||||
| Contrôleur — bloc | Exécuter le programme avec processeur, registres, RAM intégrée et six faces d’entrée/sortie. Fonctionner après retrait du terminal. | Profil d’essai : quatre registres 8 bits, RAM 1 Kio, 64 instructions/tick ; état complet conservé. |
|
||||
| Terminal — bloc | Écrire, vérifier et assembler le code, lire/écrire les disquettes ; observer registres, RAM, ports et instruction exécutée, démarrer, arrêter et essayer pas à pas. | Connexion DEVICE adjacente ; document de travail dans le contrôleur. Pas-à-pas à préciser avant code. |
|
||||
| Afficheur — bloc | Présenter dans le monde les textes, mesures et résultats des programmes sur une surface rectangulaire extensible. | Huit lignes/bloc retenues ; plafond 8 × 4 panneaux et 16 colonnes/bloc proposés. |
|
||||
| Disquette — objet | Conserver, transporter et partager un programme ; le charger dans un contrôleur. | Source 16 Kio et 512 instructions proposées ; pas de copie de RAM, retrait possible après chargement. |
|
||||
|
||||
Le terminal sert à programmer et diagnostiquer ; il n’a pas besoin de rester
|
||||
attaché à chaque contrôleur pour que celui-ci fonctionne. Processeur et RAM
|
||||
sont intégrés au contrôleur. Aucun bloc supplémentaire d’assemblage, lecteur de
|
||||
disquette ou module RAM n’est nécessaire au premier ensemble.
|
||||
L’afficheur rend le fonctionnement visible ; ce socle de trois blocs ne signifie
|
||||
pas que chaque circuit redstone doive obligatoirement les utiliser tous.
|
||||
|
||||
Pour l’expansion, l’auteur retient un **noyau dont la casse provoque la disparition
|
||||
sans objet récupérable et libère une charge collective pour le serveur**. Les
|
||||
météorites sont une voie de découverte demandée, avec d’autres voies à définir.
|
||||
Le noyau n’est plus une pièce transportable à installer ou recharger dans la
|
||||
machine. L’installation utilise les charges communes et les ressources apportées.
|
||||
**Galactium nomme l’ensemble du système d’expansion**, pour lequel une interface
|
||||
est demandée ; son accès par le terminal est proposé ci-dessous. Dépôt et ancrage
|
||||
restent à concevoir sans imposer un nouveau bloc au premier automate redstone.
|
||||
Un premier bus local adressé est désormais proposé dans le dossier V0.1 ;
|
||||
réseaux entre contrôleurs, catalogues publics et appareils de construction
|
||||
restent en réserve. Le terminal de stockage en titane reste une fonction distincte.
|
||||
|
||||
Pour le premier langage, proposer le nécessaire à un automate : lire et écrire
|
||||
les ports, charger et stocker la RAM, calculer, comparer, faire un saut, attendre
|
||||
et arrêter. Les mnémotechniques et détails sont proposés dans le
|
||||
[jeu d’instructions](redstone-language-instructions.md). Un compteur
|
||||
d’impulsions commandant une porte, conservé après rechargement, suffirait à
|
||||
vérifier l’utilité du premier ensemble avec les blocs redstone ordinaires.
|
||||
|
||||
## Particuleur et commande des effets
|
||||
|
||||
Le particuleur est un **appareil complémentaire**, utilisable sans ordinateur.
|
||||
Un objet inséré au clic droit choisit le type de particules ; le signal règle
|
||||
le débit, avec 0 pour l’arrêt et une émission croissante de 1 à 15. L’insertion
|
||||
par dropper est demandée, le hopper accolé est proposé. Le contrôleur peut
|
||||
piloter l’intensité et l’alimentation ; un transfert direct d’échantillon exige
|
||||
une fonction d’inventaire définie. Le [contrat du particuleur](audit-redstone-26.3.md#particuleur--contrat-clair-et-montages)
|
||||
précise décisions, propositions et limites de l’ancien code. Son portage,
|
||||
la table des effets et la consommation éventuelle restent à réaliser ou décider.
|
||||
|
||||
## Disquettes de langage redstone
|
||||
|
||||
**Support proposé par l’auteur : des disquettes de programmes redstone.** Les
|
||||
disques musicaux fournissent le parallèle d’un contenu porté par un objet ; la
|
||||
disquette donne une forme physique au code que l’on conserve et transmet.
|
||||
|
||||
Parcours proposé : fabriquer une disquette vierge, écrire le code au terminal,
|
||||
le vérifier et l’assembler dans ce même terminal, l’enregistrer sur la disquette,
|
||||
puis charger le programme dans un contrôleur. Le terminal pourrait relire et
|
||||
modifier le code. Conserver la source avec le programme assemblé permettrait
|
||||
d’étudier ce que l’on a trouvé ou reçu.
|
||||
|
||||
Une première version pourrait contenir un programme par disquette, avec un nom
|
||||
et une étiquette. La copie demanderait un support vierge ; ses modalités et son
|
||||
coût restent à définir. **Les programmes sont obtenables, partageables,
|
||||
duplicables et revendables**, notamment pour les montages de farming. Les
|
||||
disquettes peuvent en porter des copies, rangées dans des coffres ou exposées
|
||||
dans des cadres. Leur capacité et leur recette ne sont
|
||||
pas fixées.
|
||||
|
||||
Les ruines pourraient conserver des disquettes nommées d’après leur ancienne
|
||||
fonction : pompage, aiguillage, porte de maintenance ou protocole spatial.
|
||||
Leur code consultable et les effets du montage permettent d’étudier l’installation.
|
||||
La direction actuelle des salles exclut les panneaux explicatifs ; les éventuelles
|
||||
annotations du code ne constituent pas un cours obligatoire ni un guide affiché
|
||||
dans la pièce. Ces contenus précis restent des propositions de butin.
|
||||
|
||||
La proposition V0.1 transporte le programme sans RAM ni état d’exécution et
|
||||
conserve le programme dans le contrôleur après retrait de la disquette. Les
|
||||
gestes, limites et copies sont détaillés dans la
|
||||
[fiche disquette](redstone-language-composants.md#disquette--transporter-un-programme).
|
||||
Les déblocages et droits d’une installation demeurent ceux du serveur, même
|
||||
quand son programme est copié. Aucun support n’est implémenté ici.
|
||||
|
||||
## Architecture du contrôleur à définir
|
||||
|
||||
La vision conserve la piste d’un ordinateur 8 bits. Dans la proposition, les
|
||||
valeurs internes peuvent aller de 0 à 255 ; les intensités redstone des faces
|
||||
restent de 0 à 15. Le passage entre les deux doit être explicite et compréhensible.
|
||||
|
||||
Le contrôleur aurait un compteur d’instruction, des registres, des indicateurs
|
||||
de comparaison et une RAM dont le programme peut lire et modifier les cases.
|
||||
Le profil V0.1 propose 1 Kio de RAM, des adresses sur 16 bits, quatre registres
|
||||
A/B/C/D et un programme séparé de la RAM. Ces choix ne sont pas encore validés
|
||||
par un prototype. Un processeur 8 bits n’impose pas une RAM de 256 octets.
|
||||
|
||||
Les six faces sont les ports physiques. V0.1 propose OFF, IN, OUT ou DEVICE
|
||||
par face, avec les directions du monde comme adresses. Une communication série
|
||||
entre contrôleurs demanderait un protocole : signaux, rythme, transfert d’un
|
||||
octet et comportement en cas d’interruption.
|
||||
|
||||
**Base de travail proposée : conserver l’état complet.** Programme, RAM,
|
||||
registres, compteur d’instruction et état d’attente seraient enregistrés pour
|
||||
retrouver la machine après un rechargement. Les six faces assureraient d’abord
|
||||
les échanges redstone ordinaires ; le protocole série supplémentaire reste en
|
||||
réserve. Il faudra distinguer sauvegarder un bloc en place, le déplacer,
|
||||
copier son programme et cloner sa mémoire. La capacité de RAM et le comportement
|
||||
lors d’une coupure d’alimentation éventuelle restent à définir.
|
||||
|
||||
## Premier usage redstone à dessiner
|
||||
|
||||
Exemple avec la **syntaxe proposée V0.1**, non implémentée :
|
||||
|
||||
```text
|
||||
loop:
|
||||
IN A, NORTH
|
||||
ST 0x10, A
|
||||
LD B, 0x10
|
||||
OUT SOUTH, B
|
||||
WAIT 1
|
||||
JMP loop
|
||||
```
|
||||
|
||||
Ce petit exercice lit le signal nord, l’enregistre à une adresse de RAM, le
|
||||
relit et le reproduit au sud. Il illustre registres, mémoire, ports et boucle.
|
||||
Un programme suivant pourrait comparer un seuil, compter des impulsions ou
|
||||
temporiser une porte. Le nord doit être configuré IN et le sud OUT ; `WAIT 1`
|
||||
reprend au prochain tick chargé. Les règles complètes sont dans la proposition
|
||||
d’instructions, sans exécution réelle de ce programme dans le présent chantier.
|
||||
|
||||
La future exécution devra avoir un budget borné par tick et un comportement
|
||||
défini à l’arrêt, à l’erreur, au déchargement et au redémarrage. Les programmes
|
||||
agissent par les fonctions autorisées des appareils. Les opérations d’expansion
|
||||
restent soumises aux ressources, destinations et validations du serveur.
|
||||
|
||||
## Liaisons avec la progression et les lieux
|
||||
|
||||
**Décision déjà acquise :** l’étape du donjon de Sanctuary Island débloque les
|
||||
portails vers les Cavernes de façon collective et permanente. Tous les joueurs,
|
||||
y compris les nouveaux arrivants, peuvent ensuite fabriquer leur portail.
|
||||
Copier une inscription ne remplace pas cette étape ; sa validation initiale
|
||||
et l’usage de l’objet de quête restent à définir.
|
||||
|
||||
Les waystones, expansions et accès à l’End peuvent partager une écriture et des
|
||||
notions tout en conservant leurs propres appareils et règles. Aucune nouvelle
|
||||
condition d’accès à l’End ni obligation de programmer chaque waystone n’est
|
||||
fixée ici. Les joueurs doivent pouvoir profiter des installations communes ;
|
||||
la programmation comme spécialité volontaire est une proposition à discuter.
|
||||
|
||||
**Direction retenue : apprendre par des installations concrètes dans les salles
|
||||
secrètes de l’île.** Terminaux, contrôleurs, afficheurs et disquettes se combinent
|
||||
à des coffres, hoppers et comparateurs. Le joueur découvre un système en action,
|
||||
peut en suivre les raccords et comprendre comment le reproduire chez lui.
|
||||
L’effet spectaculaire doit venir de ce que fait la construction. De grandes
|
||||
salles peuvent présenter plusieurs systèmes de farming avec ordinateurs,
|
||||
**sans panneaux explicatifs**. Les joueurs suivent les stocks, les transferts,
|
||||
les signaux et les programmes pour comprendre. Les afficheurs montrent les
|
||||
sorties utiles à l’installation, sans devenir un tutoriel. Les dialogues des
|
||||
futurs visiteurs ne remplacent pas ce mode de découverte.
|
||||
|
||||
Le programme lui-même devient un bien échangeable : une copie peut être utilisée,
|
||||
modifiée ou revendue, sans transmettre les ressources ni les droits de l’installation.
|
||||
Sa vente devra respecter le contrat de transaction du serveur comme les autres
|
||||
biens. L’ordinateur est un système central de Sanctuary, pas seulement le bouton
|
||||
d’ouverture des continents.
|
||||
|
||||
Exemple proposé pour l’atelier : un coffre alimente une chaîne de hoppers ; un
|
||||
comparateur transmet le remplissage, le contrôleur commande un arrêt au seuil
|
||||
et l’afficheur en montre l’état. Le signal du comparateur ne fournit pas un
|
||||
inventaire exact. Une disquette pourrait transmettre le programme du montage.
|
||||
La grande salle d’expansion, évoquée aussi comme « salle de transformation »,
|
||||
étend cette découverte à l’ouverture des continents. Son nom détaillé, son
|
||||
équipement initial et ce qui doit être rééquipé restent à définir. Le donjon
|
||||
garde le rôle de stabiliser l’accès collectif aux Cavernes. Ces exemples ne
|
||||
livrent encore aucun appareil ni objet au butin.
|
||||
|
||||
Les connaissances, recensements et activités du Blocodex peuvent alimenter des
|
||||
usages à définir. Une palette connue, un stock observé et une matière engagée
|
||||
dans une opération restent des informations distinctes. Les relevés ne donnent
|
||||
pas automatiquement un droit de prélèvement ou d’accès aux stocks d’autrui.
|
||||
|
||||
La nouvelle direction relie aussi les palettes du Blocodex aux advancements
|
||||
Minecraft et Sanctuary pour ouvrir des achats au **Black Market, le shop du
|
||||
serveur**. Cette éligibilité commerciale est une intégration future, distincte
|
||||
de la mémoire personnelle des blocs actuellement livrée. Elle ne garantit ni
|
||||
le stock ni le paiement, et ne débloque pas implicitement une recette. Les
|
||||
conditions personnelles ou collectives restent à définir dans la
|
||||
[conception des informations et de l’économie](vision.md#informations-à-découvrir).
|
||||
Les cartes au trésor visent des coffres réels sur des îles déjà générées ; elles
|
||||
ne consomment pas de charge ni n’ouvrent à elles seules de nouveau territoire.
|
||||
|
||||
La cosmologie des Indoors contrôlés et des Backrooms orphelines peut prolonger
|
||||
ces expériences. Le ballast et la traduction des opérations en espaces restent
|
||||
à concevoir ; une erreur de programmation ne provoque pas automatiquement une
|
||||
Backroom ni une altération de sauvegarde.
|
||||
|
||||
### Programmes à thème et fonctions des lieux
|
||||
|
||||
**Proposition en discussion**, à la demande d’articuler Galactium avec Indoors,
|
||||
Backrooms, Cavernes, Lost City, Nether et End. Chaque famille transmettrait une
|
||||
opération caractéristique, réutilisable dans les constructions des joueurs.
|
||||
Les noms ci-dessous sont des noms de travail lisibles, pas des instructions
|
||||
d’assembleur définies ni des traductions galactiques officielles.
|
||||
|
||||
| Lieu ou espace associé | Programme proposé | Fonction distinctive et installation |
|
||||
| --- | --- | --- |
|
||||
| Lost City et ateliers anciens | **Régie — coordonner** | Piloter une séquence entre plusieurs circuits : pompage, portes, éclairage ou manutention. Un poste de contrôle et des raccords permettent de comprendre les dépendances d’une ancienne infrastructure. |
|
||||
| Cavernes | **Sonde — mesurer** | Relever les couches et rechercher des indices de gisement dans un périmètre mesuré par une station de prospection construite. Le joueur choisit où sonder puis interprète le relevé ; portée, résolution et coût restent à concevoir. |
|
||||
| Indoors | **Volume — délimiter** | Définir un intérieur borné, conserver sa référence et l’associer à un accès. Le programme équipe une installation qui crée ou gère cet espace ; il ne copie pas automatiquement son contenu. |
|
||||
| Backrooms | **Trace — retrouver** | Lire une référence orpheline et poser des repères de retour depuis une station ou des balises. Le programme aide l’exploration ; la restauration d’un espace ou le rapatriement de son contenu ne sont pas des effets acquis. |
|
||||
| Nether | **Conversion — transformer** | Conduire une transformation matérielle précise : ressources d’entrée, étapes de traitement et produit de sortie dans une installation thermique. La recette donnerait sa fonction particulière au montage ; aucun alliage, nouveau carburant ou rendement n’est encore choisi. |
|
||||
| End | **Liaison — adresser** | Identifier deux extrémités et établir une relation entre elles. Une première application pourrait transmettre un signal entre deux ancres construites ; un passage ou un transfert demanderait ensuite son propre appareil et ses règles. |
|
||||
|
||||
Lost City reste une famille de vestiges, pas une nouvelle dimension. Les villes
|
||||
sont principalement destinées aux expansions ; Sanctuary Island conserve ses
|
||||
lieux secrets et ses passages souterrains. Un atelier isolé pourrait déjà
|
||||
contenir un exemple de Régie. Les Cavernes restent destinées au minage, sans
|
||||
ajouter de mineshaft ni de parking à leur génération : la station de prospection
|
||||
serait construite par les joueurs. Son relevé local se distingue du Blocodex,
|
||||
qui conserve des connaissances et des recensements avec une couverture donnée ;
|
||||
ce nouveau capteur n’est pas implémenté par les mesures existantes.
|
||||
|
||||
Le lien Volume/Trace reprend la cosmologie définie : un Indoor possède une
|
||||
référence et un usage contrôlés ; une Backroom conserve des espaces et de la
|
||||
matière dont le contexte a disparu. Trace pourrait en éclairer des indices sans
|
||||
expliquer automatiquement leur origine. Le ballast reste une trace à concevoir
|
||||
des opérations économiques ; il n’est pas assimilé aux charges d’expansion.
|
||||
Les objets d’accès des Indoors liés à la mailbox, l’entrée des Backrooms par le
|
||||
lit et la récupération des objets perdus gardent leurs contrats à définir.
|
||||
Les pool rooms physiques de l’île ne deviennent pas des Backrooms par cette
|
||||
seule association.
|
||||
|
||||
Les disquettes portent de vrais programmes modifiables qui combinent des
|
||||
opérations offertes par les installations. Copier un programme transmet le
|
||||
savoir-faire ; l’appareil, les ressources et les déblocages du serveur déterminent
|
||||
ses effets possibles. Les exemples peuvent être utilisés puis étudiés sans
|
||||
imposer à chaque joueur d’écrire tous les programmes. Les lieux de découverte
|
||||
et les conditions d’apprentissage restent à choisir ; les six familles ne
|
||||
forment pas une campagne obligatoire dans un ordre fixé.
|
||||
|
||||
Exemple de combinaison ultérieure : Sonde fournit un relevé pour préparer une
|
||||
expédition ; Conversion prépare une ressource définie par la recette du montage ;
|
||||
Régie coordonne son alimentation et sa demande d’expansion. Après ouverture, une
|
||||
installation de Liaison pourrait relier des équipements des deux territoires.
|
||||
Cette combinaison serait une possibilité avancée, pas un prérequis aux premières
|
||||
expansions. Les autres fonctions gardent des usages autonomes.
|
||||
|
||||
Le donjon de Sanctuary débloque toujours les portails des Cavernes collectivement
|
||||
et définitivement ; Sonde n’est pas nécessaire pour gagner cet accès. Aucun
|
||||
nouveau verrou sur les portails du Nether ou de l’End n’est adopté. Une charge
|
||||
de noyau sert pour l’instant à une expansion : coût d’un Indoor, d’une liaison ou
|
||||
d’une exploration des Backrooms à décider séparément. La reprise, les pertes et
|
||||
la récupération d’objets exigeraient leurs propres règles avant implémentation.
|
||||
|
||||
## Programmer les fonctions du jeu et construire une expansion
|
||||
|
||||
**Proposition en réponse à la demande de programmer le jeu lui-même.** Le
|
||||
programme doit pouvoir composer des comportements ayant des effets réels :
|
||||
enchaîner des actions, conserver des états, réagir aux entrées et piloter les
|
||||
fonctions offertes par son installation. La redstone forme le premier usage ;
|
||||
les capacités spatiales pourraient être une extension du même ordinateur.
|
||||
|
||||
Techniquement, cela demande de relier la machine virtuelle à des opérations du
|
||||
moteur Sanctuary. L’assembleur reste le langage de calcul. Les périphériques
|
||||
offrent les actions possibles et des résultats consultables : disponibilité,
|
||||
état, demande acceptée ou refusée, progression, fin. Le programme peut choisir
|
||||
et composer ces opérations au lieu de simplement déclencher un menu prédéfini.
|
||||
|
||||
Deux niveaux sont à distinguer dans la conception : piloter le générateur de
|
||||
continents existant avec des paramètres calculés par le programme, puis, dans
|
||||
un éventuel chantier ultérieur, permettre de programmer des règles de forme ou
|
||||
de composition du terrain. Le second niveau demanderait son propre contrat de
|
||||
génération déterministe et bornée ; il n’est pas fourni par l’API actuelle.
|
||||
|
||||
### Une construction qui donne une capacité au contrôleur
|
||||
|
||||
Proposer une **installation multibloc** composée de blocs ordinaires autour du
|
||||
contrôleur. Sa forme serait reconnue par Sanctuary et lui donnerait accès aux
|
||||
opérations d’expansion, en utilisant une charge de la réserve collective.
|
||||
Le contrôleur exécute, le terminal permet de programmer et l’afficheur présente
|
||||
les résultats ; cette installation spatiale ajoute une capacité particulière
|
||||
à ce socle. Sa reconnaissance et ses effets
|
||||
constituent une mécanique proposée, non un comportement vanilla.
|
||||
|
||||
| Partie de l’installation — proposition | Rôle concret à donner à sa construction |
|
||||
| --- | --- |
|
||||
| Socle et cadre orienté | Définir le point de référence et les directions desservies. |
|
||||
| Éléments conducteurs ou de stabilisation | Définir une capacité de l’installation, éventuellement la taille maximale d’expansion ; matériaux, forme et relation à la capacité à choisir. |
|
||||
| Coffres et acheminement par hoppers | Apporter les ressources effectivement engagées dans l’opération ; quantités et consommation à définir. |
|
||||
| Contrôleur, terminal et disquette | Calculer la demande, exécuter la procédure et consulter son état. |
|
||||
| Leviers, lampes et sorties redstone | Commander le démarrage et rendre visibles attente, manque de ressources et achèvement. |
|
||||
|
||||
Le dépôt pourrait utiliser les coffres de la construction ; son orientation et
|
||||
son ancrage restent à définir. Aucun noyau intact n’est nécessaire dans le montage.
|
||||
Un premier appareil compact pourrait être agrandi ou relié à d’autres équipements
|
||||
selon des règles à concevoir.
|
||||
L’habillage architectural resterait libre autour des pièces fonctionnelles :
|
||||
la salle ancienne sert de modèle rééquipable, et la machine est reproductible
|
||||
ailleurs. Le plan exact, les matériaux et les dimensions restent des décisions
|
||||
ouvertes ; aucune grande forme rituelle obligatoire n’est validée ici.
|
||||
|
||||
### Bloc graine et météorites
|
||||
|
||||
**Direction retenue : casser le noyau le fait disparaître sans bloc ni fragment
|
||||
à ramasser. Une charge d’expansion devient disponible pour tout le serveur.**
|
||||
Le serveur émet un message en écriture galactique ; texte, traduction, son et
|
||||
répétition de cette réaction restent à choisir. La phrase poétique proposée au
|
||||
départ est abandonnée. Aucun sens canonique n’est attribué au message.
|
||||
|
||||
Cette direction remplace les propositions de noyau récupérable, de pièce à
|
||||
enfermer dans une installation et de recharge du même objet. La charge persiste
|
||||
indépendamment du découvreur et de toute machine, même après déconnexion ou
|
||||
redémarrage. Elle ne devient pas un objet à ranger ou à échanger. L’intention
|
||||
est de rendre son usage collectif ; les moyens d’accès aux noyaux encore intacts
|
||||
et le choix de qui peut engager une charge restent à définir.
|
||||
|
||||
« Bloc graine » reste une description de travail. Le nom du bloc reste ouvert ;
|
||||
KOR n’est qu’une proposition. **Galactium est le nom retenu par l’auteur pour
|
||||
tout le système d’expansion**, qui comprend la réserve de charges, les programmes,
|
||||
les installations et leurs opérations. Ce nom ne désigne pas une monnaie ou une
|
||||
quantité dans un inventaire. Les noms inventés pour Sanctuary ne sont pas des
|
||||
traductions officielles de Minecraft.
|
||||
|
||||
| Voie de découverte | Statut et intérêt proposé |
|
||||
| --- | --- |
|
||||
| Noyau de météorite | Piste demandée. Trouver un bloc étrange dans une enveloppe rocheuse ; anciens impacts et chutes observables sont deux formes possibles à choisir. |
|
||||
| Installation ancienne | Proposition. Découvrir un noyau dormant sur place et le briser pour libérer sa charge ; la construction explique son ancien usage. |
|
||||
| Reconstitution sur place | Piste ultérieure non validée. Les éventuels ingrédients doivent avoir leur propre provenance, puisque la casse d’un noyau ne donne aucun fragment. |
|
||||
|
||||
Le rapprochement avec le cube originel reste une hypothèse de lore. Rareté,
|
||||
disponibilité renouvelable, fréquence et effets des météorites restent ouverts.
|
||||
La charge ne remplace pas l’objet de quête débloquant collectivement les portails
|
||||
de minage ; elle n’accorde pas automatiquement une recette de noyau. Aucun bloc,
|
||||
message galactique ou événement météorique n’est implémenté ici.
|
||||
|
||||
### Afficheurs diégétiques, textes dynamiques et statistiques
|
||||
|
||||
**Direction retenue : afficher les textes sur des afficheurs diégétiques, intégrés
|
||||
au monde.** Ils peuvent transmettre les communications de Galactium et présenter
|
||||
des statistiques actualisées. Le support retenu est un **panneau afficheur
|
||||
extensible** : plusieurs panneaux posés côte à côte forment un grand écran.
|
||||
La recette et l’apparence précise restent à choisir.
|
||||
|
||||
#### Panneaux extensibles et surface rectangulaire
|
||||
|
||||
**Un bloc afficheur offre huit lignes de texte.** Les panneaux se raccordent
|
||||
dans un même plan, avec la même orientation, par leurs côtés. La surface commune
|
||||
est un **carré ou un rectangle plein** : chaque emplacement de son emprise doit
|
||||
être occupé. Une diagonale seule, un angle de mur, une forme en L ou en T et un
|
||||
rectangle troué ne constituent pas un écran unique.
|
||||
|
||||
Le raccord visuel de type **CTM** conserve les coins et la bordure extérieure,
|
||||
en effaçant les bordures internes. Le texte traverse les raccords comme sur une
|
||||
seule surface ; chaque bloc n’affiche pas une copie indépendante du contenu.
|
||||
CTM décrit ici le résultat visuel souhaité ; le choix technique du rendu reste
|
||||
à faire lors de l’implémentation.
|
||||
|
||||
Pour un écran de largeur W et de hauteur H, mesurées en blocs, la capacité est
|
||||
de **8 × H lignes**, chacune utilisant la largeur W. Ajouter en largeur allonge
|
||||
les lignes ; ajouter en hauteur augmente leur nombre. La taille des caractères
|
||||
reste constante. Le nombre de caractères par bloc en largeur reste à calibrer
|
||||
avec les polices lisible et galactique.
|
||||
|
||||
| Largeur × hauteur | Lignes | Largeur de chaque ligne |
|
||||
| --- | ---: | --- |
|
||||
| 1 × 1 | 8 | 1 bloc |
|
||||
| 2 × 1 | 8 | 2 blocs |
|
||||
| 2 × 2 | 16 | 2 blocs |
|
||||
| 3 × 2 | 16 | 3 blocs |
|
||||
| 4 × 3 | 24 | 4 blocs |
|
||||
|
||||
**Proposition pour construire bloc par bloc :** un panneau isolé forme un écran
|
||||
1 × 1. Un panneau ajouté contre un écran devient une extension de cet écran ;
|
||||
il reste en attente tant que l’ensemble ne forme pas un rectangle plein.
|
||||
L’écran existant conserve sa surface active et son contenu. Dès que le rectangle
|
||||
est complété, l’affichage s’étend. Ainsi, pour passer de 2 × 1 à 2 × 2, le premier
|
||||
panneau de la deuxième rangée attend le second ; la forme en L intermédiaire
|
||||
n’est jamais utilisée comme surface commune d’affichage.
|
||||
|
||||
La taille maximale, le débordement du texte, l’édition, les connexions au
|
||||
contrôleur, la casse d’un panneau et la rencontre de deux écrans déjà configurés
|
||||
restent à préciser. Agrandir ou reconfigurer l’écran doit préserver son contenu ;
|
||||
une fusion ne doit pas écraser silencieusement le programme ou les données
|
||||
d’un autre écran. Les rendez-vous, statistiques et textes utilisent la même
|
||||
surface d’affichage.
|
||||
|
||||
#### Connexions aux fonctions de Minecraft — proposition
|
||||
|
||||
**Direction retenue : l’afficheur programmable est un écran à usage général.**
|
||||
Le programme du contrôleur décide de ce qu’il affiche et de son évolution à
|
||||
partir des données et entrées disponibles. Les joueurs peuvent écrire, modifier,
|
||||
enregistrer sur disquette et partager leurs propres applications. La liste des
|
||||
usages est ouverte.
|
||||
|
||||
**Premiers usages retenus par l’auteur : chronomètre, calendrier, jauge de
|
||||
coffre et progression.** Ils deviennent des programmes de départ utilisables et
|
||||
modifiables. Leurs sources et raccordements restent à construire. Les autres
|
||||
exemples ci-dessous illustrent des applications possibles de ce même écran.
|
||||
|
||||
| Usage retenu | Première application proposée | Point restant à préciser |
|
||||
| --- | --- | --- |
|
||||
| Chronomètre | Deux entrées de départ et d’arrivée, durée de la session affichée. | Source de temps, arrêt/reprise et attribution éventuelle d’un record. |
|
||||
| Calendrier | Heure et date du serveur, âge de la partie et rendez-vous préécrits. | Convention de calendrier, fuseau et mise en page. |
|
||||
| Jauge de coffre | Comparateur raccordé à une jauge de remplissage. | Configuration et rendu des seize niveaux du signal. |
|
||||
| Progression | Avancement d’un objectif mesurable, par exemple ressources réunies pour un projet collectif. | Définition de l’objectif, source du relevé et valeur à atteindre. |
|
||||
|
||||
Deux usages complémentaires sont proposés, avant de choisir le premier ticket :
|
||||
un panneau autonome pour des contenus simples, et une sortie du contrôleur pour
|
||||
les mises en page programmées.
|
||||
|
||||
**Panneau autonome.** Il conserve un texte préparé et peut lire une entrée
|
||||
redstone. Le signal peut piloter un nombre, une jauge ou un choix parmi des
|
||||
textes configurés par le joueur. L’édition, le côté d’entrée et les conflits
|
||||
avec une connexion au contrôleur restent à définir. Une première application
|
||||
serait un coffre, son comparateur et une jauge sur l’afficheur.
|
||||
|
||||
Le comparateur vanilla donne une intensité de remplissage entre 0 et 15. Cette
|
||||
mesure ne suffit pas à compter exactement les objets ni à identifier leur type.
|
||||
La jauge doit conserver cette précision limitée. Une quantité exacte demanderait
|
||||
une lecture d’inventaire distincte, encore à concevoir.
|
||||
[Référence Mojang sur le comparateur](https://www.minecraft.net/nb-no/article/taking-inventory--redstone-comparator).
|
||||
|
||||
**Écran du contrôleur.** Proposition minimale : relier directement une face du
|
||||
contrôleur à l’arrière d’un panneau appartenant à l’écran. Ce raccord adresse
|
||||
toute la surface rectangulaire. Le terminal sert à écrire et charger le programme,
|
||||
le contrôleur conserve les calculs et la RAM, l’afficheur présente le résultat.
|
||||
Des opérations d’écriture de texte à une ligne/colonne, de nombres, d’effacement
|
||||
et de changement de page seraient à définir dans le langage. Aucun nom
|
||||
d’instruction n’est fixé ici.
|
||||
|
||||
La capacité actuellement définie est une grille de texte adressable de huit
|
||||
lignes par bloc en hauteur, sur toute la largeur du rectangle. Le programme
|
||||
compose librement ses libellés, nombres et glyphes, puis modifie leur position
|
||||
ou leur contenu. Couleurs, graphismes, interaction directe avec la surface et
|
||||
caractéristiques typographiques supplémentaires restent à concevoir.
|
||||
|
||||
Un même matériel peut ainsi accueillir un compteur, un tableau économique,
|
||||
une interface de boutique, un puzzle ou un petit jeu utilisant cette grille.
|
||||
Le contrôleur exécute les calculs avec sa RAM ; le terminal édite le programme ;
|
||||
l’afficheur présente le résultat. Les entrées redstone permettent déjà de
|
||||
concevoir des interactions par boutons et leviers, sans décider d’un écran tactile.
|
||||
|
||||
Le programme compose les données et opérations que Minecraft et Sanctuary lui
|
||||
exposent par les raccordements définis. Lire un stock, acheter un lot ou demander
|
||||
une expansion exige toujours la source ou le service correspondant. Le texte
|
||||
affiché ne remplace pas l’état autoritaire du serveur. Ces contrats donnent aux
|
||||
programmes des capacités combinables, dont les applications ne sont pas limitées
|
||||
à celles prévues par les auteurs du mod.
|
||||
|
||||
Cette liaison de données est distincte de l’intensité redstone : les caractères
|
||||
et tableaux ne sont pas transmis implicitement par une simple poudre alimentée.
|
||||
Le même contrôleur pourrait utiliser ses autres faces pour des entrées et
|
||||
sorties ordinaires. Les câbles de données, écrans distants et connexions de
|
||||
plusieurs écrans restent des extensions à étudier.
|
||||
|
||||
| Construction proposée | Entrées et usage de l’écran |
|
||||
| --- | --- |
|
||||
| Réserve ou silo | Comparateur vers jauge de remplissage ; mesure analogique limitée aux niveaux disponibles. |
|
||||
| Voie de fret | Détection redstone d’un passage vers indication d’occupation ou comptage des impulsions ; la destination affichée est configurée. |
|
||||
| Stand de tir | Signal du bloc cible vers jauge de précision ; le contrôleur peut mémoriser les scores d’une session. |
|
||||
| Parcours chronométré | Deux entrées pour départ et arrivée ; le contrôleur mesure une session et conserve un record. Temps réel ou ticks doivent être explicitement choisis. |
|
||||
| Salle d’énigmes construite par les joueurs | Boutons et leviers vers programme, symboles et résultat d’une combinaison ; les textes sont préparés par le constructeur. |
|
||||
| Affichage de livre | Piste de lecture d’un livre fourni au montage, pour afficher une page ou un texte sur le grand écran ; raccordement et accès au livre restent à concevoir. |
|
||||
| Place ou atelier collectif | Données du calendrier, statistiques ou progression d’un projet vers tableau local ; chaque donnée demande sa source serveur définie. |
|
||||
|
||||
La cible vanilla produit une intensité liée à la proximité du centre de l’impact.
|
||||
Sa valeur peut fournir l’entrée du stand de tir proposé.
|
||||
[Référence Mojang sur la cible](https://www.minecraft.net/tr-tr/article/block-week--target).
|
||||
|
||||
Un signal ne fournit pas automatiquement l’identité d’un participant : les
|
||||
compteurs et chronomètres peuvent d’abord décrire une session ou une installation.
|
||||
L’attribution personnelle d’un record demanderait une règle et une source
|
||||
d’identification supplémentaires. Les capteurs d’inventaire, lectures de livres,
|
||||
statistiques et données Sanctuary sont des connexions futures à spécifier ;
|
||||
l’afficheur ne découvre pas automatiquement toutes les données du monde.
|
||||
|
||||
Les textes composés par les joueurs pour leurs installations sont distincts de
|
||||
la sélection des communications de Galactium décrite ci-dessous. Ces usages
|
||||
et modes de connexion sont des propositions, sans nouvelle fonction implémentée.
|
||||
|
||||
La conception s’étend aussi à des [boutiques construites avec des blocs
|
||||
vanilla](vision.md#boutiques-construites-avec-des-blocs-vanilla). Un afficheur y
|
||||
présenterait une offre, son prix et les lots réellement disponibles. Ces valeurs
|
||||
demandent un contrat de commerce dédié ; la jauge analogique de coffre ne suffit
|
||||
pas à établir une offre ou à encaisser un achat.
|
||||
|
||||
#### Combinaisons émergentes
|
||||
|
||||
**Direction retenue : permettre aux joueurs de composer de nouveaux usages
|
||||
avec les mêmes blocs et programmes.** Une mesure de remplissage, un temps,
|
||||
un objectif ou une opération de commerce doit pouvoir être utilisé par le
|
||||
programme pour afficher, comparer, mémoriser puis commander une action.
|
||||
Les quatre applications de départ fournissent des exemples à réutiliser.
|
||||
|
||||
Le contrôleur garde l’état nécessaire à ces enchaînements : une session en
|
||||
cours, un seuil atteint, une demande déjà traitée. Le partage d’une disquette
|
||||
permet de reprendre un programme et d’adapter ses paramètres à une autre
|
||||
construction. Les protocoles et capacités concrètes restent à définir.
|
||||
|
||||
| Combinaison proposée | Construction obtenue |
|
||||
| --- | --- |
|
||||
| Jauge de coffre + deux seuils + sortie redstone | **Réserve régulée** : demander le réapprovisionnement au seuil bas et arrêter au seuil haut. Deux seuils distincts évitent les bascules continuelles autour d’une seule valeur. |
|
||||
| Progression + dépôt collectif + afficheur | **Chantier commun** : montrer les apports validés et ce qui manque pour l’objectif choisi, puis indiquer qu’il est atteint. L’affichage ne construit pas automatiquement le bâtiment. |
|
||||
| Calendrier + chronomètre + entrées de départ/arrivée | **Parcours à sessions** : horaires préparés, compte à rebours, durée de la session et réouverture suivante. Les records personnels demanderaient toujours une identification supplémentaire. |
|
||||
| Chronomètre + compteur en mémoire + boutons | **Atelier partagé** : distribuer des numéros de passage, afficher le tour courant et la durée d’usage, puis libérer la place. Le numéro désigne un tour, sans attribuer automatiquement une identité au joueur. |
|
||||
| Boutique + stock enregistré + calendrier | **Marché à horaires** : une offre financée par le stock du vendeur devient accessible aux horaires préparés. Un changement d’heure ne crée aucune marchandise. |
|
||||
| Boutique + objectif de ressources + budget du propriétaire | **Comptoir d’approvisionnement** : proposition de commande d’achat rémunérant les apports acceptés jusqu’à l’objectif ou l’épuisement du budget. Ce sens d’échange demande une extension du contrat de commerce. |
|
||||
| Mesure locale + minuterie + sorties redstone | **Poste de fret** : programmer un départ quand le chargement atteint un seuil ou qu’une durée s’est écoulée. L’occupation de la voie doit provenir d’une entrée définie. |
|
||||
|
||||
Parcours d’expérimentation proposé : construire une jauge, lui ajouter une
|
||||
commande à seuil, puis afficher l’objectif de réapprovisionnement. Le joueur
|
||||
peut ensuite employer ce montage pour son atelier ou pour alimenter un magasin.
|
||||
La fonction vient de la composition du programme, des raccordements et de
|
||||
la construction.
|
||||
|
||||
Ces exemples ne sont pas de nouveaux modes imposés à l’afficheur. Les données
|
||||
riches de dépôt, de commerce et de calendrier exigent leurs sources serveur ;
|
||||
un signal analogique ne suffit pas à déterminer les apports exacts d’un joueur.
|
||||
Les transactions conservent leurs vérifications et une opération déjà traitée
|
||||
ne doit pas être répétée au simple rechargement du programme. Les horaires de
|
||||
rendez-vous restent préparés ; cette composition ne réintroduit aucune catégorie
|
||||
de message Galactium rejetée.
|
||||
|
||||
Pour une première démonstration, réserve régulée, chantier commun et parcours
|
||||
à sessions sont des candidats proposés, à choisir une fois leurs sources et
|
||||
connexions définies. Aucun de ces montages n’est livré par ce cahier.
|
||||
|
||||
#### Contenus des afficheurs
|
||||
|
||||
La sélection des secrets de Galactium se limite à :
|
||||
|
||||
- Coordonnées et indices de localisation.
|
||||
- Morceaux de programmes galactiques.
|
||||
- Rendez-vous liés au calendrier ou au ciel, **écrits à l’avance**.
|
||||
|
||||
Les rendez-vous ont un texte, un horaire et des conditions de déclenchement
|
||||
préparés. L’affichage peut actualiser des champs issus du jeu : emplacement,
|
||||
date, heure ou temps restant, selon le message. Leur schéma exact reste à définir.
|
||||
Le caractère dynamique concerne ces données et leur présentation ; le rendez-vous
|
||||
et son texte sont conçus en amont.
|
||||
|
||||
**Les statistiques sont également demandées.** Exemples d’usages proposés : âge
|
||||
de la partie, quantité de ressources d’un stock consultable, avancement d’un
|
||||
projet ou nombre d’expansions ouvertes. Chaque affichage doit désigner la mesure
|
||||
et son périmètre réel. Le serveur fournit les valeurs ; les relevés partiels du
|
||||
Blocodex ne deviennent pas un total de toute l’île. Le choix des statistiques,
|
||||
leurs sources et la liaison éventuelle avec les programmes restent à concevoir.
|
||||
|
||||
La sélection exclut les astuces, fragments d’histoire des anciens, diagnostics
|
||||
de machines, appels de lieux inaccessibles, réactions aux constructions et
|
||||
demandes mystérieuses précédemment proposés comme messages de Galactium. Les
|
||||
exemples de phrases rejetés sont abandonnés. Le terminal conserve ses fonctions
|
||||
d’édition et de diagnostic technique décrites ailleurs dans ce cahier.
|
||||
|
||||
L’emplacement des afficheurs, leurs conditions d’accès et de lecture, la fréquence
|
||||
d’actualisation et la présentation galactique restent à définir. Ce cadrage
|
||||
documentaire n’ajoute encore aucun affichage ou événement au jeu.
|
||||
|
||||
### Réserve collective et interface Galactium
|
||||
|
||||
**Demande retenue : une interface pour Galactium, l’ensemble du système
|
||||
d’expansion.** Elle doit relier programmation, installation, préparation et
|
||||
usage des charges collectives. L’accès proposé est une vue du terminal existant,
|
||||
relié à une installation. Il n’ajoute pas de bloc ni de menu personnel obligatoire.
|
||||
|
||||
La base de travail est **un noyau brisé → une charge commune → une expansion**.
|
||||
Les ressources apportées à la machine restent un coût distinct, croissant selon
|
||||
la taille du projet ; recettes et quantités sont à équilibrer. Le noyau ne se
|
||||
recharge pas. Briser un noyau alimente la réserve sans lancer de génération.
|
||||
|
||||
| Zone proposée du terminal | Informations et actions |
|
||||
| --- | --- |
|
||||
| Réserve du serveur | Charges disponibles et charges engagées dans les opérations en cours, partagées par toutes les installations. |
|
||||
| Projet de l’installation | Île de départ, direction, climat, relief, taille et emplacement candidat ; un schéma de recherche, pas une carte exacte promise avant génération. |
|
||||
| Préparation | État du montage, programme chargé, ressources présentes et manquantes, conditions restantes pour démarrer. |
|
||||
| Opération | Lancement quand les conditions sont satisfaites, état de préparation, progression et résultat ; l’opération peut être retrouvée après réouverture. |
|
||||
|
||||
Le terminal présenterait une **vue d’utilisation** de l’installation et garderait
|
||||
son **éditeur assembleur**. Le programme fournit la demande et pilote la machine ;
|
||||
la vue affiche cette même demande et son suivi. Une action à l’écran doit passer
|
||||
par le même contrat que le programme. L’interface ne remplace pas la construction,
|
||||
les ressources ou le contrôleur par un simple bouton de création de continent.
|
||||
|
||||
Ouvrir le terminal, consulter un projet ou enregistrer une disquette ne réserve
|
||||
aucune charge. La charge est engagée une seule fois lorsque le serveur accepte
|
||||
effectivement le lancement avec les ressources. Une demande refusée ne la dépense
|
||||
pas. Deux installations sollicitant la dernière charge ne peuvent pas toutes
|
||||
deux l’obtenir. Une opération acceptée conserve son identité et son engagement
|
||||
après déconnexion, fermeture ou casse du terminal ; la reprise ne paie pas à
|
||||
nouveau. Le traitement d’un échec après acceptation devra être défini avec le
|
||||
contrat de sauvegarde et de reprise, sans remboursement ni nouvelle génération
|
||||
automatiques supposés.
|
||||
|
||||
Les inscriptions et la réaction du serveur gardent l’écriture galactique.
|
||||
Compteurs, actions et conditions de fonctionnement doivent rester compréhensibles,
|
||||
avec libellés FR/EN ; la place de la transcription et du déchiffrement reste à
|
||||
dessiner. Des témoins redstone visibles sur la machine sont proposés en complément
|
||||
de l’écran. La visibilité de la réserve depuis les autres lieux et les règles
|
||||
collectives de dépense sont ouvertes : aucun vote ou droit exclusif du propriétaire
|
||||
du terminal n’est décidé ici.
|
||||
|
||||
### Parcours proposé dans le monde
|
||||
|
||||
1. Libérer une charge en brisant un noyau, ou utiliser une charge déjà disponible
|
||||
pour le serveur. Retrouver un programme et les raccords de l’installation
|
||||
ancienne, ou utiliser un programme partagé ; construire les éléments nécessaires.
|
||||
2. Préparer au terminal une demande : parent, direction, climat, relief et taille
|
||||
dans les capacités et déblocages disponibles.
|
||||
3. Chercher un emplacement et afficher un projet ainsi que son coût. Un schéma
|
||||
de recherche ne doit pas être présenté comme une carte exacte déjà générée.
|
||||
4. Apporter les matériaux et donner le départ. Le serveur valide ensemble la
|
||||
construction, l’accès, la destination et l’engagement d’une charge commune
|
||||
avec les ressources. Consulter un projet ne dépense rien.
|
||||
5. Le contrôleur conserve l’identifiant de l’opération et suit sa préparation.
|
||||
Les lampes et le terminal rendent le travail lisible ; un programme repris
|
||||
suit la même opération.
|
||||
6. Une fois le continent prêt, sa cartographie et son relevé réel de ressources
|
||||
pourraient devenir des résultats consultables. Leur production reste à réaliser.
|
||||
|
||||
Ces échanges pourraient passer par des registres de périphérique, décrits en
|
||||
assembleur et affichés en galactique. Un descripteur ou des valeurs sur plusieurs
|
||||
octets devront représenter les coordonnées, tailles et quantités qui dépassent
|
||||
255 ; un signal redstone ordinaire reste compris entre 0 et 15.
|
||||
|
||||
### Ce que le moteur fournit déjà et ce qui manque
|
||||
|
||||
Lecture du code d’expansion de l’alpha.23 locale : `ExpansionRuntime.Session`
|
||||
expose recherche de candidat, sondage, création, progression et reprise. Les
|
||||
diamètres admis vont de 64 à 1 024 selon les profils disponibles. Une création
|
||||
réserve durablement son emprise avant la préparation progressive des chunks.
|
||||
Ces fonctions sont actuellement utilisées par des commandes opérateur.
|
||||
|
||||
Le candidat proposé n’effectue pas à lui seul toute l’admission. Celle-ci peut
|
||||
attendre jusqu’à 30 secondes dans le chemin actuel ; elle devra être adaptée
|
||||
avant tout appel depuis un programme. La préparation des chunks est déjà suivie
|
||||
par une file. La réutilisation de certains vides certifiés demande actuellement
|
||||
un rechargement du monde. Ce parcours technique ne constitue pas encore une
|
||||
machine utilisable en survie.
|
||||
|
||||
Le raccord futur devra introduire une demande asynchrone avec identité persistante,
|
||||
plutôt qu’appeler la création à chaque tour de boucle. Il reste à définir le
|
||||
contrat entre charge collective, ressources engagées, réservation du terrain,
|
||||
interruption et reprise. La réserve collective et l’interface Galactium n’existent
|
||||
pas encore dans ce moteur. Les règles de sauvegarde et les constructions
|
||||
existantes sont conservées ;
|
||||
une opération enregistrée n’est pas annulée par simple retrait de la disquette.
|
||||
|
||||
Le Blocodex peut fournir des connaissances et des observations ; les stocks
|
||||
réservés dans l’installation relèvent du contrat de consommation à construire.
|
||||
Le relevé de la nouvelle région intervient après génération, avec sa couverture.
|
||||
Le programme ne relance pas des graines pour garantir un quota de minerai.
|
||||
L’éventuelle trace de ballast appartient au futur contrat économique.
|
||||
|
||||
Le déblocage collectif des portails de minage reste une décision distincte.
|
||||
Ce multibloc ne fixe pas les conditions de la première expansion, ni la forme
|
||||
des waystones ou des portails vers les autres dimensions.
|
||||
|
||||
## Prochaines décisions
|
||||
|
||||
1. Apprentissage des glyphes et des instructions ; lien exact avec les Endermen.
|
||||
2. Connexion du terminal au contrôleur et chargement du programme depuis la disquette.
|
||||
3. Taille de RAM, premier montage, rythme d’exécution et règles de reprise.
|
||||
4. Capacité, recette, copie, lecture et chargement des disquettes ; contenus découverts dans les ruines.
|
||||
5. Construction d’expansion : pièces fonctionnelles, forme, capacité, ressources et opérations programmables ; confirmer ou ajuster la proposition multibloc.
|
||||
6. Galactium : nom du noyau et voies de découverte, message galactique et forme des météorites ; disponibilité des charges et règles de dépense collective.
|
||||
7. Interface : accès depuis le terminal, disposition, lisibilité FR/EN et lien avec l’éditeur assembleur ; valider la proposition avant implémentation.
|
||||
|
||||
Le [cahier des lieux](structures-conception.md) conserve les décisions générales
|
||||
et le [document de vision](vision.md) le périmètre du projet.
|
||||
@@ -1,674 +0,0 @@
|
||||
# Sanctuary — machines que l’on construit
|
||||
|
||||
**Exploration WG-26 ; aucun nouveau bloc, recette ou format de sauvegarde livré.**
|
||||
Le principe précisé par l’auteur est **l’assemblage de blocs identiques en une
|
||||
version agrandie de leur fonction** : 27 fours en cube plein deviennent un
|
||||
grand appareil de cuisson ; 27 barils deviennent un **Fût**. La précédente
|
||||
proposition de four creux entouré de briques est remplacée. **Fourneau** est
|
||||
le nom proposé ici pour les 27 fours, encore à valider avec l’auteur.
|
||||
|
||||
Le **grand baril de fermentation** est le nom retenu pour traiter plusieurs
|
||||
stacks d’une même recette. Le kitchen oven d'It's Alive ! est regroupé dans le
|
||||
Fourneau, dont les recettes et la chaleur restent à concevoir. **Métablit** est
|
||||
le nom proposé par l’auteur ; sa fonction d’établi collectif reste en discussion.
|
||||
|
||||
Les collections deviennent visibles sur des présentoirs : **9 objets par face
|
||||
de bloc, 81 par façade 3 × 3 et 324 objets distincts sur quatre façades**. Les
|
||||
méga-pistons sont eux aussi retenus en conception : **9 pistons en façade 3 × 3,
|
||||
course de 3 blocs**, avec variante collante. Les autres assemblages décrits plus
|
||||
bas distinguent désormais la **Trémie retenue : 9 hoppers à plat et 45 cases**,
|
||||
et le **Carillon retenu pour ses accords et courtes séquences, formé par un
|
||||
assemblage adjacent choisi**. Les appareils supplémentaires restent proposés ; aucun n’est livré.
|
||||
|
||||
La **clé à molette dorée** est retenue pour **orienter, assembler volontairement
|
||||
et désassembler**. Les blocs restent indépendants à la pose ; le joueur choisit
|
||||
de les réunir avec la clé. L’auteur confirme ce choix pour les hoppers :
|
||||
un montage de neuf hoppers doit pouvoir conserver ses neuf circuits au lieu
|
||||
de devenir une Trémie. Ses gestes et sa fabrication sont à concevoir.
|
||||
|
||||
Le coffre 3 × 3 × 3 évoqué sert ici de référence au principe de construction.
|
||||
Aucun coffre de ce type n’a été identifié dans le code courant ni les documents
|
||||
consultés. Le stockage en titane reliant 128 coffres est une autre fonction ;
|
||||
ce cahier ne présente donc pas ce coffre comme déjà implémenté.
|
||||
|
||||
## Clé à molette dorée — assembler, désassembler et orienter
|
||||
|
||||
**Retenu par l’auteur :** un objet spécial, la **clé à molette dorée**, permet
|
||||
d’assembler volontairement des blocs compatibles, de dissocier un multibloc,
|
||||
d’intervenir sur les pistons et de tourner des blocs comme les escaliers et
|
||||
les barils. Nom anglais proposé :
|
||||
**Golden Wrench**. Recette, obtention, durabilité et vitesse de récupération
|
||||
restent à définir ; la couleur dorée ne fixe pas un coût ni un enchantement.
|
||||
|
||||
### Retrouver les blocs indépendants
|
||||
|
||||
Pour répondre à l’exemple des 27 fours, « casser le multibloc » est interprété
|
||||
ici comme **dissocier la machine sur place**. Les 27 fours restent posés et
|
||||
retrouvent un fonctionnement individuel. Le même principe s’applique aux neuf
|
||||
bases du méga-piston, y compris sa variante collante. Récupérer un bloc dans
|
||||
l’inventaire est une autre action ; la clé ne compacte pas les 27 composants
|
||||
en un objet machine transportable.
|
||||
|
||||
**Exigence : la dissociation persiste.** Une forme compatible n’est pas forcément
|
||||
un assemblage actif. Les blocs séparés doivent rester indépendants après une
|
||||
mise à jour voisine, un déchargement ou une réouverture, même si le cube de
|
||||
fours est toujours complet. Le joueur peut les réunir à nouveau avec la même
|
||||
clé : cette fonction est validée. Le geste exact de réassemblage reste à choisir.
|
||||
|
||||
**Règle commune retenue : poser ne suffit pas à assembler.** Les blocs
|
||||
nouvellement posés gardent leur usage individuel ; le joueur les réunit
|
||||
volontairement avec la clé. Fours, hoppers et pistons peuvent ainsi rester
|
||||
des composants indépendants. Proposition de retour visuel : une forme compatible
|
||||
est mise en évidence avec l’outil avant de modifier les circuits.
|
||||
|
||||
Le démontage conserve **l’état actuel** des matériaux et du travail. Il ne
|
||||
restaure pas les anciens inventaires d’avant assemblage : du combustible a
|
||||
pu être consommé et des recettes terminées depuis. Les contenus encore
|
||||
présents sont redistribués ou restitués une seule fois ; les matières d’un
|
||||
résultat terminé ne reviennent pas en plus de ce résultat. La répartition
|
||||
dans les cases des fours/barils individuels et le traitement d’un surplus
|
||||
restent à définir. Si une séparation ne peut pas conserver le contenu, elle
|
||||
doit attendre une récupération du stock, sans supprimer les objets.
|
||||
|
||||
Pour le méga-piston, le démontage concerne **l’ensemble réel de la mécanique**,
|
||||
avec ses bases, sa tête et sa tige. Une tête déployée ne devient pas un piston
|
||||
supplémentaire à récolter. Proposition de premier comportement : intervenir
|
||||
une fois le piston rétracté et immobile ; une demande pendant le mouvement
|
||||
ne déplace pas instantanément sa charge. La récupération d’un piston ordinaire
|
||||
avec la clé est également à prévoir, avec sa durée et son usure propres.
|
||||
|
||||
### Gestes proposés
|
||||
|
||||
| Situation | Geste à essayer | Résultat attendu |
|
||||
| --- | --- | --- |
|
||||
| Blocs indépendants compatibles | Sélection puis validation avec la clé ; combinaison de touches à définir | Former volontairement une machine commune à partir des blocs choisis. |
|
||||
| Bloc indépendant orientable | Clic droit avec la clé | Passer à l’orientation admissible suivante, avec un aperçu du sens obtenu. |
|
||||
| Multibloc actif | Sneak + clic droit avec la clé | Mettre en évidence tout le groupe visé et le dissocier sur place, selon son état de fonctionnement. |
|
||||
| Bloc à récupérer | Minage avec la clé | Récupérer le bloc admissible ; durée, outils compatibles et cas des pistons à préciser. |
|
||||
| Composant d’un multibloc actif, demande de rotation | Clic droit avec la clé | Montrer l’assemblage concerné ; dissocier avant de tourner un seul composant, afin de ne pas désorganiser silencieusement la machine. |
|
||||
|
||||
Les trois fonctions sont retenues ; ces gestes précis restent proposés. La clé tenue distingue l’intention
|
||||
de réglage des clics usuels : ouvrir un baril, accorder un bloc musical,
|
||||
configurer un contrôleur ou insérer un objet dans le particuleur.
|
||||
|
||||
Le **Carillon** emploie aussi la clé pour réunir volontairement les blocs
|
||||
adjacents choisis. La séquence de sélection et de validation reste à dessiner ;
|
||||
elle respecte le choix d’un assemblage adjacent volontaire.
|
||||
Le geste d’assemblage doit se distinguer de la rotation d’un bloc indépendant.
|
||||
|
||||
### Orientations et autres usages à explorer
|
||||
|
||||
| Cible | Fonction retenue ou piste |
|
||||
| --- | --- |
|
||||
| **Escalier — demandé** | Tourner sa direction en conservant le matériau ; le raccord aux voisins est recalculé. Retourner haut/bas est une possibilité supplémentaire à choisir séparément. |
|
||||
| **Baril — demandé** | Réorienter son ouverture en conservant son contenu et son identité. Les 27 barils d’un Fût doivent d’abord être dissociés pour être réglés individuellement. |
|
||||
| **Piston — piste de réglage** | Changer la direction une fois rétracté, sans déplacer les blocs devant lui ni modifier sa variante normale/collante. |
|
||||
| **Hopper — piste** | Tourner le bec vers une sortie admissible, pour raccorder notamment le hopper situé sous la Trémie. Cela ne choisit pas une destination distante. |
|
||||
| **Bûche ou pilier — piste** | Changer l’axe en place, avec les états permis par le bloc. |
|
||||
| **Répéteur, comparateur, distributeur — pistes** | Corriger la direction en conservant les réglages ou objets. La nouvelle connexion suit la construction obtenue. |
|
||||
|
||||
L’admissibilité dépend du bloc : support, raccords, pièces liées et état de
|
||||
fonctionnement. La liste n’accorde pas automatiquement une rotation à tous
|
||||
les blocs ou multiblocs du pack. Pour les appareils à entrée/sortie relative,
|
||||
une rotation réévalue les connexions et retire les anciens signaux des faces
|
||||
abandonnées.
|
||||
|
||||
**Contrôleur :** une éventuelle rotation à la clé garde les adresses cardinales
|
||||
du programme et la configuration de ses faces ; tourner sa façade ne remappe
|
||||
pas `NORTH` vers `EAST`. CPU et RAM sont conservés. Cette extension demanderait
|
||||
un contrôleur arrêté ou en pause et une vérification des connexions avant
|
||||
reprise. Elle complète les [fiches des composants](redstone-language-composants.md),
|
||||
sans changer le langage ni les commandes main vide.
|
||||
|
||||
Les opérations restent des modifications de construction validées côté
|
||||
serveur, avec les mêmes droits de modification du lieu. La clé ne termine
|
||||
pas une recette et ne relance pas un programme pour effectuer une rotation.
|
||||
|
||||
## Fourneau — 27 fours et une chauffe commune
|
||||
|
||||
L’intérêt est de **traiter une charge de matériaux dans un même appareil chauffé**.
|
||||
Le joueur assemble les 27 fours, apporte le combustible et choisit la charge.
|
||||
Le contrôleur peut ensuite organiser les cycles, mais le four doit être utilisable
|
||||
à la main et avec une commande redstone simple.
|
||||
|
||||
### Forme retenue et aspect proposé
|
||||
|
||||
**27 blocs de four ordinaires forment un cube plein de 3 × 3 × 3**, y compris
|
||||
le bloc central. Ni cavité laissée vide, ni briques de remplacement, ni foyer
|
||||
supplémentaire à fabriquer. Proposition : la forme complète est reconnue
|
||||
comme compatible avec un seul appareil, doté d’une grande façade et d’une
|
||||
interface commune. Une disposition dissociée à la clé reste indépendante
|
||||
jusqu’à un réassemblage volontaire.
|
||||
Le choix de sa façade, la règle d’orientation des blocs et son raccord visuel
|
||||
restent à éprouver.
|
||||
|
||||
Trois couches identiques, `F` représentant un vrai bloc de four :
|
||||
|
||||
```text
|
||||
Base Milieu Dessus
|
||||
FFF FFF FFF
|
||||
FFF FFF FFF
|
||||
FFF FFF FFF
|
||||
```
|
||||
|
||||
La grille de matériaux est un inventaire de l’appareil ; elle ne nécessite pas
|
||||
neuf cellules vides à l’intérieur de ce cube. Les 27 fours construits donnent
|
||||
la forme et le coût de l’ensemble, pas automatiquement 27 fois la vitesse ou
|
||||
le rendement. Le regroupement doit conserver les objets, combustibles et états
|
||||
présents avant formation, sans continuer simultanément 27 cuissons indépendantes.
|
||||
Le contrat de formation/démontage sera défini avant toute conversion en jeu.
|
||||
|
||||
**Nom proposé : Fourneau.** « Méga-four » reste descriptif pendant la discussion.
|
||||
Le nom ne désigne pas un assemblage de hauts fourneaux : le matériau demandé
|
||||
est bien le four ordinaire. Aucun registre existant n’est renommé dans ce cahier.
|
||||
|
||||
### Deux usages à comparer dans le même appareil
|
||||
|
||||
| Usage proposé | Charge et résultat |
|
||||
| --- | --- |
|
||||
| **Cuire une fournée / Batch smelting** | Les neuf cases accueillent des matières à cuire selon les recettes admises. Plusieurs pièces partagent un cycle de chauffe ; les résultats restent ceux des recettes. Aucun mélange accidentel ne devient automatiquement un alliage. |
|
||||
| **Recette à chaud / Heat crafting** | La grille 3 × 3 forme une seule recette composée : ingrédients, positions ou proportions définis par la recette, durée et résultat. Les matériaux sont traités ensemble. C’est la piste pour les alliages et de nouvelles transformations. |
|
||||
|
||||
Le mode serait choisi explicitement à la façade, avant de lancer la charge.
|
||||
Cela rend compréhensible la différence entre deux minerais cuits séparément
|
||||
et deux matériaux destinés à une transformation commune. Un livre de recettes
|
||||
ou un programme peut aider à préparer le plateau ; ni l’un ni l’autre n’est
|
||||
requis pour les premières utilisations manuelles.
|
||||
|
||||
**Capacité d’essai proposée :** neuf cases d’entrée et un bac de résultats
|
||||
acceptant plusieurs types d’objets. Neuf cases ne signifient pas que neuf stacks
|
||||
complètes doivent fondre instantanément. Pour un premier cycle de cuisson,
|
||||
prélever une unité par case occupée admissible ; pour une recette composée,
|
||||
prélever les quantités définies par cette recette. Les réserves restantes
|
||||
peuvent alimenter les cycles suivants.
|
||||
|
||||
Une recette à chaud ressemble ainsi à un craft qui demande aussi une cuisson :
|
||||
**disposer → chauffer → récupérer**. Le premier essai n’a pas besoin de degrés
|
||||
réels, de pression, de gaz ou d’une interface de simulation industrielle.
|
||||
Des familles de chauffe pourront apparaître si certaines recettes le justifient.
|
||||
|
||||
### Source de chaleur et intérêt du volume
|
||||
|
||||
Une seule source de chaleur alimente l’appareil entier. La proposition de
|
||||
départ est **un foyer et une réserve de combustible communs**. Le foyer peut
|
||||
consommer plusieurs unités au fil de la fournée ; « une source » désigne son
|
||||
emplacement, pas une unité de combustible valable indéfiniment.
|
||||
|
||||
La durée et le combustible consommé doivent dépendre du travail demandé. Le
|
||||
gain du grand four peut venir de la chauffe partagée et du traitement par lots,
|
||||
avec un rendement à comparer à une installation de fours ordinaires. Aucun
|
||||
multiplicateur de vitesse, de combustible ou de minerais n’est arrêté ici.
|
||||
|
||||
Un contact avec une source extérieure, un foyer permanent ou le
|
||||
[siphon thermique proposé](redstone-language-objets-et-cristaux.md#siphon-thermique)
|
||||
sont d’autres pistes. Ils demanderaient leurs propres règles ; ils ne donnent
|
||||
pas implicitement une production illimitée gratuite au premier four.
|
||||
|
||||
### Cycle proposé
|
||||
|
||||
1. Compléter le cube de 27 fours et former volontairement l’installation avec
|
||||
la clé, selon le geste à choisir. Un cube dissocié reste indépendant tant
|
||||
que le joueur n’a pas choisi de le réunir à nouveau.
|
||||
2. Charger le plateau, choisir le mode et alimenter le foyer.
|
||||
3. Prévisualiser les ingrédients utilisés et les résultats, puis lancer. Le
|
||||
serveur vérifie la recette, le combustible et la place nécessaire au résultat.
|
||||
4. Isoler la charge en cours ; les apports ultérieurs attendent le cycle suivant.
|
||||
Le foyer chauffe pendant la durée de travail, avec une animation et un état
|
||||
lisibles en façade.
|
||||
5. Déposer le résultat une seule fois dans le bac, prêt au retrait. Un cycle
|
||||
n’éjecte pas son contenu dans le vide parce que le bac voisin est plein.
|
||||
|
||||
Pour ce prototype proposé, les ingrédients engagés sont gardés dans la charge
|
||||
en cours jusqu’à la transformation. Une structure cassée arrête le traitement ;
|
||||
avant transformation, les matériaux de la charge restent récupérables et le
|
||||
combustible déjà brûlé reste dépensé. Après transformation, seul le résultat
|
||||
correspondant existe. Une reprise ne doit pas refaire le même lot.
|
||||
|
||||
La politique de refroidissement après coupure ou rupture est à éprouver. Pour
|
||||
les matières industrielles, aucune destruction, explosion ou surveillance
|
||||
constante n’est ajoutée par défaut. Les recettes culinaires conservent au
|
||||
contraire les fenêtres de cuisson et de récupération prévues par It's Alive ! :
|
||||
le méga-four ne garantit pas automatiquement la meilleure qualité d’un plat.
|
||||
|
||||
### Redstone, convoyeurs et programmes
|
||||
|
||||
La proposition distingue **autoriser les cycles** et **fournir la chaleur**.
|
||||
Un levier peut permettre les fournées automatiques ; le combustible reste requis.
|
||||
Une coupure empêche les nouveaux engagements et met le traitement en pause selon
|
||||
la règle thermique choisie. Le fonctionnement manuel ne dépend pas d’un CPU.
|
||||
|
||||
Un comparateur pourrait donner le remplissage du bac de résultats. Les états
|
||||
« prêt », « chauffe », « manque de matière » et l’avancement exact pourraient
|
||||
passer par une interface de données vers un contrôleur et un afficheur, sans
|
||||
faire signifier plusieurs mesures différentes au même signal de comparateur.
|
||||
|
||||
L’automatisation demande des **points d’accès identifiés** : entrées du plateau,
|
||||
combustible, sortie. Leur forme et leurs faces seront définies avec le dessin
|
||||
retenu, en respectant les ingrédients des recettes positionnelles. Un convoyeur
|
||||
amène des drops à un collecteur ; un coffre collé ne les aspire pas spontanément.
|
||||
Les opérations se font dans un inventaire commun, sans recopier le contenu
|
||||
dans chacun des 27 fours constitutifs.
|
||||
|
||||
Un programme pourrait remplir, vérifier, chauffer puis évacuer. Copier sa
|
||||
disquette ne copie ni le four, ni le stock, ni les recettes débloquées. Le
|
||||
programme thématique **Conversion**, déjà proposé pour le Nether, pourrait
|
||||
montrer un savoir-faire de ce type ; il n’impose pas de verrou Nether au four.
|
||||
|
||||
## Recettes et place dans la progression
|
||||
|
||||
L’auteur souhaite ouvrir la possibilité des alliages. Leur composition et leurs
|
||||
usages restent à concevoir ; la présence du silver et du titane dans la vision
|
||||
ne les transforme pas automatiquement en alliages fabriquables. Les équipements
|
||||
Anomaly restent infrabricables selon la règle retenue.
|
||||
|
||||
Pour éprouver le four, préparer deux familles de recettes : une cuisson par lot
|
||||
qui conserve les rendements usuels des recettes choisies, puis **une recette à
|
||||
plusieurs ingrédients dont le produit a un usage clair**. La première révèle le
|
||||
confort de la chauffe commune ; la seconde justifie la nouvelle fonction du four.
|
||||
Le choix du premier alliage doit venir avec la pièce, le bloc ou l'équipement
|
||||
auquel il sert, sans ajouter plusieurs métaux seulement pour remplir une grille.
|
||||
|
||||
Le maçon ou un autre métier approprié pourrait travailler dans un atelier qui
|
||||
emploie ce four, selon les futures tâches rémunérées des villageois. L’appareil
|
||||
traite une charge ; le travailleur se déplace, approvisionne et organise une tâche
|
||||
de métier.
|
||||
|
||||
### Une cuisson commune avec It's Alive !
|
||||
|
||||
**Direction retenue : le méga-four accueille les recettes de cuisson d'It's
|
||||
Alive ! et reprend le rôle du kitchen oven.** Il n’y a pas besoin de maintenir
|
||||
deux appareils qui font la même cuisson dans le pack. Les poêles, marmites,
|
||||
moulins, séchoirs et autres procédés distincts gardent leurs rôles ; cette
|
||||
décision ne transforme pas toute préparation culinaire en cuisson au four.
|
||||
|
||||
It's Alive ! reste autonome : ingrédients, recettes, pages, qualité et suivi
|
||||
culinaire restent sous sa responsabilité. L’appareil de chauffe commun doit
|
||||
donc pouvoir être fourni avec ce mod ou une dépendance partagée explicitement
|
||||
déclarée, sans imposer le reste de Sanctuary pour jouer à sa cuisine. Le choix
|
||||
du module qui porte le four appartient au ticket d’architecture, avant code.
|
||||
|
||||
Les recettes métallurgiques et culinaires utilisent le même service de charge
|
||||
et de chauffe, avec leurs propres résultats et règles. Le regroupement ne crée
|
||||
pas de contamination des aliments, de bonus d’alliage sur les plats ou de
|
||||
nouvelle étape de nettoyage implicite. Le modèle mélangeant différentes durées
|
||||
et fenêtres culinaires dans une même fournée devra être essayé ; un premier
|
||||
cycle homogène peut servir de témoin avant de promettre toutes les combinaisons.
|
||||
|
||||
## Fût — 27 barils et un stockage paginé
|
||||
|
||||
**Nom retenu : Fût**, à la place de giga-baril. **27 barils ordinaires assemblés
|
||||
en cube plein de 3 × 3 × 3** donnent un stockage commun parcouru par **pages
|
||||
et/ou défilement**. L’intégration des cases à l’inventaire custom reste
|
||||
explicitement ouverte à la demande de l’auteur.
|
||||
|
||||
Le matériau et le gabarit sont désormais définis. Le dessin du fût, son orientation,
|
||||
la capacité, les tailles supplémentaires et la présentation des pages restent
|
||||
à préciser. Proposition : les barils contenant déjà des objets conservent leurs
|
||||
quantités lors du regroupement ; le nouvel inventaire ne les copie pas en plus
|
||||
des anciens. Ce contrat doit aussi fonctionner au démontage.
|
||||
|
||||
- Le nombre de pages présente une capacité réelle et finie ; faire défiler
|
||||
n’ajoute pas des emplacements sans limite.
|
||||
- Deux joueurs ou deux ouvertures accèdent au même contenu autoritaire. Tourner
|
||||
une page change la vue, pas l’identité ou le nombre des objets stockés.
|
||||
- Les hoppers, s’ils sont raccordés, travaillent sur le stockage selon leurs
|
||||
faces et filtres ; ils ne dépendent pas de la page affichée par un joueur.
|
||||
- L’agrandissement ou la casse doit conserver une représentation unique des
|
||||
matières. Réduire la capacité ne peut pas faire disparaître le contenu des
|
||||
pages devenues invisibles. Le comportement concret est à définir avant code.
|
||||
- Une page de conteneur n’est pas une rangée supplémentaire de l’inventaire
|
||||
personnel ; la progression des cases du joueur garde son contrat.
|
||||
|
||||
Le Fût est un **lieu de stockage**. Le terminal en titane reste l’outil
|
||||
de consultation d’un réseau de coffres ; les deux capacités ne sont pas confondues.
|
||||
Le baril de fermentation décrit ci-dessous est une machine de transformation,
|
||||
pas une autre page ou un mode caché du dépôt.
|
||||
|
||||
## Grand baril de fermentation — une recette, plusieurs stacks
|
||||
|
||||
**Nom retenu : grand baril de fermentation.** Il traite plusieurs stacks d’une **même recette de fermentation**
|
||||
dans un grand baril. Une recette peut comporter plusieurs ingrédients ; « même
|
||||
recette » ne signifie pas « un seul type d’objet ». Les proportions doivent
|
||||
rester celles de la recette, multipliées par la taille du lot engagé.
|
||||
|
||||
La composition exacte du grand baril n’est pas déduite de celle du Fût de stockage.
|
||||
Proposition : une cuve de fermentation construite, reconnaissable à sa bonde
|
||||
et à son point de remplissage. Un premier gabarit 3 × 3 × 3 peut être étudié,
|
||||
sans en faire la taille imposée de tous les procédés culinaires.
|
||||
|
||||
1. Choisir la recette et charger ses ingrédients jusqu’à la quantité souhaitée.
|
||||
2. Fermer le lot : afficher la recette, les quantités utilisées et les résultats
|
||||
attendus. Les ingrédients en excès restent hors du lot engagé.
|
||||
3. Lancer une fermentation commune, avec une date de départ et les conditions
|
||||
définies par It's Alive !.
|
||||
4. Récupérer la production ou poursuivre un vieillissement si la recette le prévoit.
|
||||
|
||||
Pour le premier contrat proposé, ajouter des ingrédients après le départ ne
|
||||
leur donne pas rétroactivement l’âge du lot. Ils attendent un nouveau cycle.
|
||||
La fermentation de plusieurs stacks suit un même procédé ; rendement, durée,
|
||||
volume utile et conditions ne sont pas fixés par le nombre de stacks seul.
|
||||
|
||||
Le vieillissement du vin et l’affinage déjà prévus gardent leurs règles. La bière
|
||||
ne reçoit pas automatiquement le bonus de vieillissement du vin. Les pages
|
||||
culinaires et l’apprentissage des recettes restent ceux d'It's Alive !.
|
||||
|
||||
La relation entre temps réel, arrêt du serveur et progression du lot est à
|
||||
choisir explicitement : une date sauvegardée peut permettre une évolution
|
||||
hors ligne, mais ce n’est pas déduit du calendrier de Sanctuary. Recharger une
|
||||
cuve ne doit ni recommencer un lot ni produire deux fois ses résultats.
|
||||
|
||||
Le rôle de la redstone serait d’autoriser le lancement et les transferts, avec
|
||||
un état lisible sur place. Couper un fil n’arrête pas automatiquement un procédé
|
||||
biologique : les règles de conservation et d’interruption appartiennent à la
|
||||
recette. Un afficheur peut montrer recette, durée et quantité sans rendre un
|
||||
ordinateur obligatoire pour faire fermenter une charge.
|
||||
|
||||
## Métablit — proposition d’établi collectif
|
||||
|
||||
L’auteur propose le nom **Métablit**. La fonction explorée ici est un établi
|
||||
construit où **un projet reste en place**, avec son plan et les matériaux que
|
||||
plusieurs joueurs apportent. Un joueur doit aussi pouvoir l’utiliser seul.
|
||||
|
||||
La surface peut montrer les pièces déjà réunies et celles qui manquent. Fermer
|
||||
l’interface ne rend pas tout au dernier visiteur ; le projet appartient à la
|
||||
table et ses accès suivent les règles de l’installation. Une fabrication
|
||||
réussie consomme ses matériaux et produit un seul résultat récupérable.
|
||||
|
||||
**Exemple de projet à éprouver :** rassembler les quatre bateaux nécessaires
|
||||
au Booat, les voir dans le projet, puis réaliser la recette. Le Métablit ne
|
||||
devient pas obligatoire pour cette recette déjà prévue, et n’en change pas
|
||||
les ingrédients par défaut. D’autres assemblages peuvent révéler si cette
|
||||
forme de travail apporte assez pour justifier le bloc.
|
||||
|
||||
Un plan utilise les connaissances et recettes effectivement accessibles.
|
||||
La table ne débloque pas le catalogue mondial de schematics, ne remplace pas
|
||||
les conditions galactiques du build et ne construit pas gratuitement l’ouvrage
|
||||
ailleurs dans le monde. Taille de la surface, grille, gestes et distinction
|
||||
avec crafter/table de craft restent à préciser : la seule augmentation de
|
||||
taille d’un menu ne suffit pas à expliquer son intérêt.
|
||||
|
||||
## Présentoirs de collection — objets visibles sur les façades
|
||||
|
||||
L’auteur retient les archives modulaires et précise leur rôle de **collection
|
||||
visible**, proche de celui des item frames. **Présentoir** est un nom proposé
|
||||
pour le bloc à neuf emplacements ; ce nom et sa fabrication restent ouverts.
|
||||
|
||||
**Comptage confirmé avec l’auteur :**
|
||||
|
||||
| Disposition | Objets distincts exposables |
|
||||
| --- | --- |
|
||||
| Une face de bloc | 3 × 3 petits emplacements, soit **9 objets**. |
|
||||
| Une façade de 3 × 3 blocs | Neuf faces de bloc, soit **81 objets visibles simultanément** en grille 9 × 9. |
|
||||
| Quatre façades de 3 × 3 | **81 objets différents par face, soit 324 au total**. |
|
||||
|
||||
Une façade plate 3 × 3 suffit : la construction en volume n’est pas obligatoire.
|
||||
Dans une disposition cubique avec quatre murs, les angles présentent deux
|
||||
faces distinctes. Leurs objets ne sont pas des copies vus depuis deux côtés :
|
||||
chaque face possède ses propres neuf emplacements. Le nombre de blocs supports
|
||||
ne suffit donc pas à déterminer la capacité. Dessus et dessous n’ajoutent pas
|
||||
implicitement d’autres emplacements aux 324 confirmés.
|
||||
|
||||
Ce sont des objets visibles dans le monde, **sans pagination des 81 objets de
|
||||
la façade**. Le Fût conserve sa pagination de stockage ; l’afficheur programmable
|
||||
conserve ses huit lignes de texte par bloc. Ces trois appareils ont des fonctions
|
||||
et des rendus distincts.
|
||||
|
||||
### Contenus à exposer
|
||||
|
||||
| Demandé par l’auteur | Présentation à concevoir |
|
||||
| --- | --- |
|
||||
| Cartes, plans et recettes | Montrer leur support, avec la carte miniature ou une couverture lisible. Une consultation peut ouvrir le contenu déjà accessible. |
|
||||
| Photos | Conserver la carte ou le support photographique réel et afficher l’image correspondante ; ne pas fabriquer neuf copies de la carte. |
|
||||
| Disquettes | Icône, nom du programme ou étiquette. L’exposition ne charge pas le programme dans un contrôleur. |
|
||||
| Disques blancs, disques musicaux/CD | Pochette ou disque visible, selon le modèle retenu. Le présentoir n’est pas automatiquement un jukebox. |
|
||||
| Spawn eggs | Œufs visibles par catégorie ou couleur, sans apparition d’entité ni activation de pouvoir de familier. |
|
||||
|
||||
**Proposition de généralisation :** accepter aussi outils, armes, armures,
|
||||
gemmes, fleurs, trophées et autres items représentables dans une case. Un
|
||||
premier principe simple serait **un objet physique par emplacement**, pas une
|
||||
stack cachée derrière chaque icône. Les cas de modèles volumineux ou de contenus
|
||||
moddés devront être éprouvés ; aucune liste universelle n’est déjà implémentée.
|
||||
|
||||
Le joueur viserait un des neuf emplacements pour insérer ou retirer son objet.
|
||||
Les gestes exacts et la consultation restent à dessiner avec l’inventaire custom.
|
||||
Exposer une recette ou un plan ne débloque pas automatiquement ses droits pour
|
||||
tout le serveur. Casser un support doit rendre les objets concernés une seule
|
||||
fois, y compris ses deux faces s’il appartient à un angle.
|
||||
|
||||
**Constructions possibles :** mur des expéditions en cartes et photos, bibliothèque
|
||||
publique de programmes, collection de disques, galerie d’œufs rares. Le rendu
|
||||
de 324 items, notamment des cartes, devra rester lisible et raisonnable à distance ;
|
||||
les niveaux de détail pourront réduire le coût sans réduire le nombre d’objets
|
||||
réels ou transformer les façades en pages invisibles.
|
||||
|
||||
## Méga-pistons — une façade mobile sur trois blocs
|
||||
|
||||
**Retenu et confirmé :** neuf pistons assemblés en façade **3 × 3** donnent
|
||||
une tête commune **3 × 3**, dont la **course est de trois blocs**. La variante
|
||||
collante est également demandée. Ce n’est pas seulement neuf pistons qui
|
||||
avancent chacun d’un bloc.
|
||||
|
||||
Proposition de formation : neuf pistons ordinaires de même orientation donnent
|
||||
le méga-piston ; neuf pistons collants de même orientation donnent sa variante
|
||||
collante. Les mélanges ne constituent pas un troisième comportement par défaut.
|
||||
La profondeur des bases et l’aspect de la tige déployée restent à dessiner.
|
||||
La clé à molette dorée permet de dissocier les neuf bases pour retrouver des
|
||||
pistons indépendants. Le cas rétracté est proposé pour le premier essai ;
|
||||
l’outil ne sépare pas neuf mouvements pendant une course commune.
|
||||
|
||||
- La redstone commande **un mouvement d’ensemble**. Alimenter plusieurs blocs
|
||||
constitutifs ne multiplie pas les poussées ni la distance parcourue.
|
||||
- La tête doit contrôler son trajet complet, pas seulement la destination à
|
||||
trois blocs : elle ne traverse pas un obstacle intermédiaire.
|
||||
- Proposition : si une partie bloque le mouvement admissible, l’ensemble
|
||||
refuse de démarrer. Les neuf cases ne se désolidarisent pas pour avancer
|
||||
indépendamment et arracher un morceau de la construction.
|
||||
- La version collante doit ramener la charge admissible avec sa tête lors de
|
||||
la rétraction. Limites, blocs adhérents et collisions restent à définir ;
|
||||
le mot « collant » ne promet pas de déplacer tous les conteneurs ou machines.
|
||||
- Les **27 cellules balayées** par la tête pendant sa course ne sont pas une
|
||||
limite validée de 27 blocs transportés. Masse poussable, nombre de blocs en
|
||||
profondeur, durée et interactions slime/miel sont des réglages distincts.
|
||||
|
||||
L’interruption en cours de déplacement, le déchargement et la casse demandent
|
||||
un état de mouvement commun, sauvegardé une seule fois. Aucun de ces détails
|
||||
ne doit être remplacé par une copie des neuf mouvements de pistons indépendants.
|
||||
|
||||
**Constructions possibles :** grande porte rétractable de trois blocs, plateforme
|
||||
élévatrice sur une course de trois blocs, mur mobile ou poste d’amenée d’une
|
||||
section de construction. Le trajet réel doit être animé et perceptible ; le
|
||||
moteur ne téléporte pas la charge à travers des blocs.
|
||||
|
||||
## Trémie — neuf hoppers et une grande surface de collecte
|
||||
|
||||
**Retenu par l’auteur :** neuf hoppers à plat, en carré **3 × 3**, recueillent
|
||||
les drops sur leur surface et réunissent **neuf fois l’inventaire d’un hopper**.
|
||||
Le hopper Java de la cible possède cinq emplacements : la Trémie dispose donc
|
||||
de **45 cases communes**, avec les limites d’empilement propres aux objets.
|
||||
Ce gain de capacité ne fixe pas un débit de sortie neuf fois supérieur.
|
||||
|
||||
**Choix explicitement demandé : la Trémie est facultative.** Les joueurs
|
||||
peuvent conserver neuf hoppers côte à côte pour construire un autre circuit.
|
||||
Chaque hopper indépendant garde ses cinq cases, son bec, ses connexions et
|
||||
sa commande redstone. La réserve de 45 cases communes et la sortie unique
|
||||
appartiennent seulement à l’assemblage actif.
|
||||
|
||||
Règle retenue : les hoppers se posent indépendamment et la clé
|
||||
à molette dorée permet de choisir leur réunion en Trémie. Les poser en 3 × 3,
|
||||
compléter un coin manquant ou approcher un autre hopper ne doit donc pas
|
||||
convertir un montage par simple voisinage. Désassembler à la clé rend les
|
||||
neuf blocs indépendants, sans les réunir à nouveau après rechargement.
|
||||
|
||||
### Sortie proposée : sous le centre
|
||||
|
||||
La sortie reste à choisir. La proposition la plus lisible est **un seul bec
|
||||
sous le bloc central**, visible dans le modèle raccordé. Les huit autres
|
||||
hoppers constituent les bords du collecteur ; leurs anciennes orientations
|
||||
ne deviennent pas huit sorties supplémentaires une fois l’ensemble formé.
|
||||
|
||||
```text
|
||||
Vue de dessus Coupe par le centre
|
||||
H H H H H H
|
||||
H C H ↓
|
||||
H H H sortie
|
||||
|
||||
C : hopper central ; sortie sous C
|
||||
```
|
||||
|
||||
Le joueur peut mettre le conteneur destinataire sous ce bec. Pour partir sur
|
||||
un côté, il place **un hopper ordinaire dessous, orienté vers la suite de son
|
||||
circuit** ; si nécessaire, une chaîne rejoint le bord du plateau. Ce hopper
|
||||
supplémentaire est un raccord indépendant avec ses cinq cases, pas une dixième
|
||||
partie obligatoire de la Trémie. La sortie n’est ni distante ni choisie dans
|
||||
le menu d’un ordinateur.
|
||||
|
||||
Autre piste : un bec latéral choisi lors de l’assemblage. Elle économise de la
|
||||
hauteur, mais demande de montrer quel bord évacue les objets et comment le
|
||||
joueur le choisit. Elle n’est pas cumulée d’office avec la sortie centrale.
|
||||
|
||||
### Fonctionnement à éprouver
|
||||
|
||||
- Le stock est commun aux neuf surfaces. Les neuf inventaires d’origine ne
|
||||
continuent pas à se transférer les mêmes objets en parallèle.
|
||||
- Dans la proposition à bec central, les extractions automatiques passent
|
||||
par ce raccord identifié ; mettre un hopper sous un angle n’ajoute pas une
|
||||
deuxième sortie cachée. Les interfaces d’insertion restent à préciser.
|
||||
- Destination pleine ou absente : les objets restent dans les 45 cases ; une
|
||||
réserve pleine laisse les nouveaux drops dans le monde, sans les effacer.
|
||||
- La collecte concerne la surface construite, pas un rayon d’aspiration
|
||||
élargi autour de l’appareil. La collecte et le débit d’évacuation seront
|
||||
réglés séparément.
|
||||
- Proposition de commande : alimenter le bloc central verrouille l’ensemble,
|
||||
selon le rôle d’arrêt du hopper. La portée électrique, le comparateur et
|
||||
les faces de commande doivent être rendus lisibles avant de fixer le contrat.
|
||||
- La formation volontaire à la clé est retenue ; une
|
||||
nappe de hoppers indépendants conserve ses circuits et ne devient pas
|
||||
obligatoirement une Trémie du fait de sa forme.
|
||||
Au démontage à la clé, le stock actuel est conservé une seule fois et les
|
||||
orientations indépendantes sont rétablies ; l’ancien stock d’avant formation
|
||||
n’est pas restauré en plus du contenu courant. La dissociation persiste.
|
||||
|
||||
**Usages :** réception sous une récolte, convergence de plusieurs convoyeurs,
|
||||
grand bac de déchargement. Le terminal peut consulter le stock ; il n’est pas
|
||||
requis pour décider où les objets ressortent.
|
||||
|
||||
## Carillon — des notes réunies en instrument
|
||||
|
||||
**Retenu par l’auteur :** réunir des blocs musicaux pour former des **accords
|
||||
ou de courtes séquences**, avec un **assemblage adjacent choisi**. Le joueur
|
||||
choisit les blocs qui appartiennent à son instrument. Ni rangée obligatoire,
|
||||
ni carré 3 × 3, ni nombre de neuf notes imposé. Le simple voisinage ne forme
|
||||
pas automatiquement un groupe. Ce choix remplace la préférence précédente
|
||||
pour une rangée ; les contraintes rectangulaires de l’afficheur ne s’appliquent
|
||||
pas au Carillon.
|
||||
|
||||
### Sélection de l’instrument et ordre de lecture
|
||||
|
||||
Proposition de gestes et de lecture, encore à éprouver :
|
||||
|
||||
1. Entrer volontairement en assemblage sur un premier bloc musical ; il sert
|
||||
de point de commande identifiable pour le groupe.
|
||||
2. Ajouter les blocs souhaités, chacun étant adjacent par une face à au moins
|
||||
un bloc déjà sélectionné. Une diagonale seule ne suffit pas dans cette
|
||||
proposition. Un voisin non sélectionné reste indépendant.
|
||||
3. **Utiliser l’ordre de sélection comme ordre initial de la séquence.** Le
|
||||
nouveau bloc n’a pas besoin de toucher la dernière note sélectionnée : il
|
||||
touche le groupe. Les branches restent possibles sans demander au moteur
|
||||
de deviner quel chemin musical suivre.
|
||||
4. Montrer cet ordre par de petits numéros ou repères pendant le réglage,
|
||||
écouter un aperçu, puis valider. Une commande de réordonnancement reste
|
||||
à dessiner pour changer la mélodie sans reconstruire l’instrument.
|
||||
|
||||
Le nombre maximal de notes, les gestes exacts et l’éventuelle construction
|
||||
en volume restent à définir. La clé à molette dorée est retenue pour réunir
|
||||
le groupe choisi et pour le désassembler.
|
||||
Des blocs musicaux non réunis gardent leur fonctionnement individuel ; deux
|
||||
carillons peuvent être voisins sans fusionner. La casse suspend le groupe
|
||||
concerné jusqu’à réparation ou redéfinition, selon un contrat à préciser,
|
||||
plutôt que de renuméroter la mélodie silencieusement.
|
||||
|
||||
Un raccord visuel discret ferait apparaître un cadre commun, en gardant chaque
|
||||
bloc accessible pour l’accorder et chaque position lisible pendant le jeu.
|
||||
Ni coque cubique ni neuf blocs supplémentaires d’habillage ne sont nécessaires
|
||||
dans cette proposition.
|
||||
|
||||
### Notes, instruments et tempo
|
||||
|
||||
Chaque bloc garde sa note et son instrument. Pour les instruments usuels,
|
||||
le support sous **chaque bloc** continue à déterminer le timbre : le groupe
|
||||
ne remplace pas tous les supports par celui du premier bloc. Les exceptions
|
||||
vanilla utilisant un instrument au-dessus doivent aussi être conservées.
|
||||
Un habillage commun ne doit pas obturer l’espace nécessaire au jeu des notes.
|
||||
Ces règles sont vérifiées dans `NoteBlock` pour la cible Java 26.3-pre-2.
|
||||
|
||||
Deux modes proposés, sélectionnables sur l’instrument avec un repère visible :
|
||||
|
||||
- **Accord :** une impulsion joue ensemble les notes du groupe.
|
||||
- **Séquence :** une impulsion joue la prochaine note, puis avance d’une case.
|
||||
Une horloge redstone donne le tempo ; sans nouvelle impulsion, la lecture
|
||||
attend. Après la dernière note, le prochain déclenchement revient au début.
|
||||
|
||||
Ainsi, un bouton permet de jouer à la main et un circuit peut commander un
|
||||
motif. Une commande locale de retour au début est à prévoir. Les clics usuels
|
||||
d’accordage et d’écoute doivent rester disponibles ; changer une note pendant
|
||||
l’édition ne déclenche pas toute la séquence involontairement.
|
||||
|
||||
Le lancement d’une phrase entière par une seule impulsion est une **variante
|
||||
à comparer**, avec un tempo interne à régler. Il ne se cumule pas implicitement
|
||||
avec le mode où chaque impulsion donne une note. Silences, répétitions d’une
|
||||
note et durées individuelles sont des pistes ultérieures, pas un séquenceur
|
||||
complet déjà décidé. Les programmes peuvent ensuite composer avec les mêmes
|
||||
commandes physiques, sans être nécessaires pour jouer un accord.
|
||||
|
||||
**Usages :** sonnerie de gare, horloge qui joue une courte mélodie, alarme
|
||||
reconnaissable ou instrument que plusieurs joueurs accompagnent. Le Carillon
|
||||
ne lit pas automatiquement les disques exposés dans un présentoir.
|
||||
|
||||
## Autres assemblages de blocs identiques à explorer
|
||||
|
||||
La recherche suit désormais le principe de regroupement précisé par l’auteur :
|
||||
une grande surface ou un volume donne une fonction commune aux blocs posés.
|
||||
Les gabarits et les noms ci-dessous sont proposés, pas retenus.
|
||||
|
||||
| Assemblage proposé | Fonction agrandie | Exemples d’usage |
|
||||
| --- | --- | --- |
|
||||
| **Veilleur : 9 observateurs en façade, 3 × 3** | Surveiller les neuf positions juste devant la façade ; montrer où un changement a eu lieu et permettre d’en récupérer l’information. | Surveillance d’une surface cultivée ; porte à combinaison de blocs. Aucune lecture à travers toute l’île ni détection de maturité s’il n’y a pas de donnée définie. |
|
||||
| **Batterie de distribution : 9 distributeurs en façade, 3 × 3** | Choisir une bouche ou enchaîner les neuf dans une séquence, avec leurs vrais objets et comportements de distribution admis. | Arrosage de positions définies avec des seaux ; spectacle de feux d’artifice. Pas de gain gratuit de projectiles par objet consommé. |
|
||||
|
||||
Le Veilleur et la batterie de distribution demandent une interface de sélection claire pour ne pas seulement
|
||||
regrouper les menus des neuf blocs.
|
||||
|
||||
La **presse à moule est retirée de la sélection** : l’auteur juge la fabrication
|
||||
déjà couverte par le crafter qu’il évoque. Le terme retranscrit « gel crafter »
|
||||
n’a pas permis d’identifier un autre composant dans les sources consultées ;
|
||||
aucun comportement d’un nouveau crafter n’est supposé ici. Le Métablit reste
|
||||
en réflexion, sans réintroduire la presse sous un autre nom.
|
||||
|
||||
Les anciennes pistes de bassin de traitement, serre climatique, écluse et
|
||||
alambic restent en réserve comme installations construites. Elles n’appartiennent
|
||||
pas automatiquement à la nouvelle famille de blocs identiques assemblés.
|
||||
Les recettes culinaires éventuelles d’un bassin ou alambic restent de la
|
||||
responsabilité d'It's Alive !.
|
||||
|
||||
## Règles communes à prévoir pour les multiblocs
|
||||
|
||||
- La construction reconnaît sa forme, ses blocs constitutifs et ses pièces utiles. Un décor voisin
|
||||
ne fait pas gagner silencieusement une capacité ou un rendement.
|
||||
- La clé à molette dorée permet une dissociation durable en blocs indépendants.
|
||||
Reconnaître une forme compatible ne la réactive pas contre le choix du joueur.
|
||||
- Règle commune retenue : la formation elle-même est volontaire, avec la clé.
|
||||
Les circuits de hoppers et les groupes de fours ordinaires restent utilisables
|
||||
dès la pose, sans devoir obtenir un outil pour empêcher leur regroupement.
|
||||
- Une installation possède un seul état de référence. Deux façades ne dupliquent
|
||||
pas les stocks ; deux machines ne revendiquent pas les mêmes blocs constitutifs.
|
||||
- Décharger un morceau suspend le fonctionnement concerné sans charger le monde
|
||||
à distance ni rejouer des cycles écoulés hors ligne.
|
||||
- Casser ou modifier l’assemblage a un effet explicite sur le travail et les
|
||||
matières ; déplacement et récupération ne copient pas une charge en cours.
|
||||
- Les constructions fonctionnent avec leurs gestes locaux ; le terminal, le
|
||||
contrôleur et l’afficheur permettent ensuite d’en coordonner plusieurs.
|
||||
|
||||
Ces règles sont des exigences pour les futurs tickets, pas un format de sauvegarde
|
||||
déjà adopté. La première validation proposée reste **un four construit à la main,
|
||||
une fournée ordinaire et une recette composée**, avec arrêt/reprise et retrait
|
||||
des résultats. Aucun monde existant n’est modifié pour ce cahier.
|
||||
@@ -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.
|
||||
-600
@@ -1,600 +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.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,650 +0,0 @@
|
||||
# Sanctuary — progression, outils et intégrations
|
||||
|
||||
**Conception en discussion, WG-26.** Ce cahier relie les outils communautaires,
|
||||
les capacités Sanctuary et les découvertes à la progression. Il ne livre aucun
|
||||
mod, plan, slot ou changement de sauvegarde. La version de référence du chantier
|
||||
reste Minecraft **26.3-pre-2, Fabric** ; chaque intégration demandera une
|
||||
vérification sur cette version exacte avant distribution.
|
||||
|
||||
## Direction retenue
|
||||
|
||||
Sanctuary est un RPG Minecraft pour une communauté d’environ vingt joueurs.
|
||||
Explorer, construire, produire, programmer et combattre donnent des moyens
|
||||
différents de progresser. Certains outils de confort deviennent des capacités
|
||||
que l’on acquiert : consultation des recettes, orientation, plans de construction
|
||||
ou informations supplémentaires. Les fonctionnalités à débloquer sont définies
|
||||
par Sanctuary ; le mod communautaire peut en fournir l’interface ou l’outil.
|
||||
|
||||
**Cadrage proposé :** laisser les réglages de présentation, de performance et
|
||||
d’accessibilité à la disposition du joueur. Shaders, textures HD et acoustique
|
||||
ne demanderaient pas d’XP et resteraient désactivables. Les correctifs du pack
|
||||
s’appliqueraient dès leur installation. L’auteur a remis en question le principe
|
||||
de « débloquer la HD » ; ce choix visuel ne devient pas un palier acquis.
|
||||
|
||||
Le nom précisé par l’auteur est **Sound Physics Remastered**. Il traite
|
||||
l’atténuation, la réverbération et l’absorption du son à travers les blocs.
|
||||
C’est une référence acoustique distincte des shaders.
|
||||
[Source du projet](https://github.com/henkelmax/sound-physics-remastered).
|
||||
|
||||
## Compétences, unlocks, galactique et prestige
|
||||
|
||||
**Cadrage retenu :** les compétences montent par paliers achetables en XP.
|
||||
Les unlocks sont des achats facultatifs, parfois uniques, parfois eux-mêmes
|
||||
répartis en paliers. Terminer ses compétences ouvre deux possibilités : acheter
|
||||
des facultés galactiques de fin de jeu, ou choisir un prestige. Il n’est pas
|
||||
nécessaire de prendre la première voie pour emprunter la seconde.
|
||||
|
||||
| Ensemble | Rôle | Condition retenue |
|
||||
| --- | --- | --- |
|
||||
| Compétences | Faire progresser les capacités du personnage : vie, faim, vitesse de minage et capacité du vein mining, portée de construction et capacité du vein building, avec respiration et inventaire déjà prévus. | Acheter les paliers ; les effets vein se débloquent avec Mining et Build, sans achat séparé. Liste définitive, plafonds et prix à arrêter. |
|
||||
| Unlocks facultatifs | Ajouter des outils pratiques : indicateur alimentaire, minimap, carte et noms des entités. | Achat unique ou par paliers, avec d’éventuels prérequis de compétence ou de découverte à définir. |
|
||||
| Facultés galactiques | Ouvrir des possibilités endgame, dont le catalogue complet des builds. | **Toutes les compétences au maximum**, puis acquisition de chaque faculté ; les autres unlocks ne sont pas tous exigés. |
|
||||
| Prestige | Recommencer la progression personnelle et faire grandir sa faction. | **Toutes les compétences au maximum** ; ni tous les unlocks, ni les facultés galactiques ne sont requis. Récompense en charges personnelles à dépenser pour débloquer des slots dans sa faction, sans nouvelles capacités bonus. |
|
||||
|
||||
Le stade galactique n’est plus conditionné au seul maximum de Build. Son
|
||||
accès ne donne pas gratuitement toutes les facultés et ne déclenche pas un
|
||||
prestige automatique. Le catalogue constitue la première faculté définie ;
|
||||
les autres restent à imaginer. Aucun coût distinct d’« entrée en mode galactique »
|
||||
n’est ajouté à ce stade.
|
||||
|
||||
La vitesse demandée ici est celle du **minage**. Aucun arbre de vitesse de
|
||||
déplacement n’est ajouté par cette discussion ; le sprint initialement verrouillé
|
||||
reste à situer dans la progression. L’exemple de coûts doublés **1, 2, 4, 8, 16,
|
||||
32, 64 niveaux** reste une piste d’équilibrage, pas sept paliers identiques
|
||||
imposés à chaque compétence ou unlock.
|
||||
|
||||
**Remise à zéro retenue : les unlocks achetés restent acquis, les compétences
|
||||
reviennent au départ et sont à remonter.** Il n’est pas nécessaire de racheter
|
||||
la minimap. Le vein mining et le vein building appartiennent aux compétences :
|
||||
leur disponibilité et leur capacité suivent donc le niveau actuel de Mining
|
||||
et de Build après le prestige. Au retour de Mining au niveau zéro, le vein
|
||||
mining redevient désactivé ; il revient en remontant Mining. La portée de pose
|
||||
et le remplissage suivent de même la progression actuelle de Build. Ils ne
|
||||
font pas partie des unlocks achetés séparément et conservés.
|
||||
|
||||
Les facultés galactiques restent soumises au maximum de toutes les compétences.
|
||||
**Proposition complémentaire :** conserver celles déjà achetées, puis réactiver
|
||||
leur usage au nouveau maximum sans les racheter. Ce traitement, l’XP restante
|
||||
et celui des collections restent à préciser. La remise à zéro des compétences
|
||||
n’efface pas le monde ni ses déblocages collectifs, comme les portails des Cavernes.
|
||||
|
||||
**Affichage retenu :** le compteur de prestiges suit le pseudo en chiffres
|
||||
romains, par exemple **Poupoutin VI** pour six prestiges. Le nom ou symbole de
|
||||
faction précède le pseudo, qui garde sa couleur personnelle choisie dans la
|
||||
palette réservée du serveur. Cet affichage n’ajoute pas de récompense mécanique.
|
||||
Voir les [règles de présentation](accueil-et-profils.md#couleur-personnelle-faction-et-prestige).
|
||||
|
||||
**Capacité initiale retenue : trois membres par faction.** Les prestiges donnent
|
||||
des **charges personnelles**, rattachées à l’UUID du joueur. Celui-ci peut les
|
||||
dépenser dans sa faction pour débloquer des places supplémentaires. Ces crédits
|
||||
d’agrandissement sont distincts des charges collectives de **Galactium** ; ils
|
||||
ne servent pas à ouvrir des continents. La capacité d’une faction n’est pas
|
||||
calculée automatiquement en additionnant les prestiges de ses membres : le
|
||||
joueur choisit de dépenser ses charges.
|
||||
|
||||
Le nombre de charges gagnées par prestige, le coût de chaque place et le devenir
|
||||
des places financées après le départ d’un membre restent à fixer. Aucun
|
||||
remboursement, retrait automatique de place ni conversion d’une charge en une
|
||||
place n’est décidé ici. Le suffixe romain continue d’indiquer les prestiges
|
||||
accomplis, indépendamment des charges déjà dépensées.
|
||||
|
||||
### Vein mining et vein building : paliers des compétences
|
||||
|
||||
**Direction retenue : ce sont des mécaniques centrales de Mining et de Build.**
|
||||
Elles se débloquent et augmentent en montant ces compétences, sans achat d’un
|
||||
unlock supplémentaire. Le joueur peut choisir de les utiliser ou de garder
|
||||
le geste bloc par bloc. La proposition précédente de deux arbres d’achats
|
||||
facultatifs séparés est remplacée par ce fonctionnement.
|
||||
|
||||
**Recommandation de conception :** conserver ce lien direct. Un palier de
|
||||
Mining ou de Build apporte ainsi un changement de geste perceptible, sans
|
||||
demander un second achat pour une progression étroitement liée. Réserver les
|
||||
achats d’unlocks séparés aux outils de confort et d’information. Le déblocage
|
||||
de l’action groupée n’en impose pas l’utilisation à chaque clic.
|
||||
|
||||
| Compétence | Repères de progression | Effet de l’action groupée |
|
||||
| --- | --- | --- |
|
||||
| Mining | Niveau 0 : vein mining désactivé. Premier repère donné : dès le niveau 1, jusqu’à **4 blocs au total**, bloc visé compris. Les niveaux suivants augmentent ce nombre ; **64 blocs** est un exemple de groupe avancé, sans niveau ni plafond définitif fixé. | Compter les blocs sélectionnés, puis commencer leur minage en même temps. |
|
||||
| Build | La portée et la capacité de remplissage évoluent avec la compétence. Niveau d’activation et nombres de blocs par palier restent à définir ; les chiffres de Mining ne sont pas automatiquement recopiés. | Remplir un trou ou une surface sur **un seul plan**, avec une limite de blocs posés. |
|
||||
|
||||
Le **vein mining commence sur tout le groupe sélectionné simultanément**.
|
||||
L’action doit donner à percevoir que tous ces blocs sont en cours de casse,
|
||||
plutôt qu’un premier bloc cassé suivi de la disparition instantanée des autres.
|
||||
Une représentation des fissures sur les blocs est une proposition visuelle à
|
||||
éprouver. L’outil choisi, les blocs concernés, le nombre à casser et la vitesse
|
||||
de Mining déterminent la durée de travail. À conditions égales, un groupe plus
|
||||
grand demande davantage de temps : casser 64 blocs ne coûte pas le temps d’un
|
||||
seul bloc. Le gain recherché vient aussi des petites manipulations évitées entre
|
||||
viser, miner, casser et récupérer chaque bloc.
|
||||
|
||||
La formule de durée reste à équilibrer : aucune somme exacte des durées vanilla,
|
||||
aucun multiplicateur ni pourcentage d’accélération n’est arrêté. Commencer le
|
||||
minage simultanément ne fixe pas encore si tous les blocs terminent leur casse
|
||||
ensemble ou selon des progressions individuelles. L’admissibilité des blocs,
|
||||
les outils et enchantements, l’usure, le moment des drops et le traitement des
|
||||
interruptions restent à spécifier, notamment si l’outil casse ou change pendant
|
||||
l’action. L’ancienne intention de veine contiguë ou de plan orienté est conservée ;
|
||||
le geste précis de sélection reste à concevoir.
|
||||
Le journal des transformations comptera les blocs effectivement cassés ou
|
||||
posés, et les entrées et sorties réelles, sans compter la sélection initiale
|
||||
comme une opération déjà accomplie.
|
||||
|
||||
Le **vein building remplit une surface plane**, y compris un trou au milieu
|
||||
d’une surface existante. Il ne remplit pas un volume en trois dimensions.
|
||||
Les cases à compléter, le plan choisi et la limite du palier doivent être
|
||||
lisibles avant la pose. Les blocs viennent de l’inventaire et les positions
|
||||
restent dans la portée acquise de Build, sans remplacer implicitement les blocs
|
||||
occupés. Orientation horizontale ou verticale, sélection du contour, délai de
|
||||
pose et cas où les matériaux manquent restent à éprouver. Le temps de casse
|
||||
groupée du vein mining n’impose pas la même animation au vein building.
|
||||
|
||||
Les limites de groupe et de portée servent des fonctions différentes ; agrandir
|
||||
la surface traitée n’allonge pas implicitement la portée. Les règles de récolte,
|
||||
de protection et d’aventure continuent de s’appliquer. Aucun coût d’XP par action
|
||||
n’est décidé en plus de l’achat des paliers de compétence.
|
||||
|
||||
### Unlocks uniques et informations par paliers
|
||||
|
||||
L’indicateur alimentaire, la minimap et la carte du monde font partie des outils
|
||||
facultatifs souhaités. Proposition : acheter d’abord une lecture simple, puis
|
||||
des informations supplémentaires lorsque cela apporte un usage distinct, par
|
||||
exemple les waypoints. Carte du monde et minimap ne dévoilent pas automatiquement
|
||||
les territoires inexplorés. Leur découpage et les intégrations exactes restent
|
||||
à choisir ; aucun achat d’affichage ne modifie la faim réelle ou le terrain.
|
||||
|
||||
**L’affichage des noms d’entités est un unlock unique retenu.** Une configuration
|
||||
permettra d’ajouter des listes de noms et des catégories personnalisées, en
|
||||
associant chaque catégorie à un ou plusieurs identifiants de mobs. Les familles
|
||||
humoristiques prévues, comme les pseudos de zombies et les noms galactiques des
|
||||
Endermen, restent des listes de départ. Priorité entre catégories qui se recoupent,
|
||||
attribution stable d’un nom et comportement lors d’une modification de configuration
|
||||
restent à définir. Débloquer l’affichage ne remplace pas les noms donnés aux
|
||||
entités par les joueurs.
|
||||
|
||||
## Capacités à relier aux découvertes
|
||||
|
||||
La sélection des fonctions est retenue ; leurs paliers, coûts et conditions
|
||||
exactes restent à dessiner. Ce tableau ne prescrit pas une campagne linéaire.
|
||||
|
||||
| Domaine | Capacité envisagée | Distinction à conserver |
|
||||
| --- | --- | --- |
|
||||
| Recettes — JEI | Révéler les recettes par familles et advancements Minecraft/Sanctuary. | Affichage d’une recette, fabrication autorisée et achat au Black Market sont trois règles différentes. |
|
||||
| Cuisine — It’s Alive ! | Débloquer des pages culinaires et compléter le livre de recettes de Kai ; conserver les pages secrètes des sorcières. | Page trouvée, recette connue, affichage JEI, ingrédients disponibles et autorisation de préparation restent à articuler. Le mod culinaire garde son autonomie. |
|
||||
| Orientation — Xaero’s Minimap et carte à intégrer | Débloquer la minimap, la carte du monde et des fonctions de waypoints, selon des paliers à définir. | Un repère n’autorise pas une téléportation et ne révèle pas automatiquement les trésors ou salles cachées. Le choix d’outil pour la carte reste ouvert. |
|
||||
| Voyage — waystones | Découvrir un ascenseur dans une île générée, reliant par exemple la surface à un laboratoire. Verticalement : ascenseur ; horizontalement : téléportation précise, coûteuse en XP. | L’usage concret enseigne le transport ; aucun palier abstrait supplémentaire n’est décidé. La minimap ne fournit pas cette capacité. |
|
||||
| Alchimie et enchantement | Relier la connaissance des procédés et les usages aux étapes de jeu. | Préciser quels procédés sont concernés avant de verrouiller des actions vanilla ou de choisir un mod. |
|
||||
| Architecture — Architext / Litematica | Ouvrir le bouton de bibliothèque dans les pouvoirs, puis acquérir des plans, dont des aqueducs, par le combat ou l’obtention de blocs associés. | Une combinaison de matériaux découverts peut proposer une architecture ; connaître le plan ne fournit ni matériaux ni pose gratuite. |
|
||||
| Nourriture — AppleSkin ou HUD Sanctuary | Débloquer un indicateur rendant lisibles faim, saturation et effet d’un aliment, avec l’habillage Sanctuary. | L’unlock facultatif est retenu ; le choix d’intégration et le détail des informations par palier restent ouverts. Les valeurs suivent les capacités réelles du joueur. |
|
||||
| Équipement | Afficher ou masquer l’état de l’armure et des outils, notamment leur durabilité restante. | Un réglage de visibilité ne répare pas l’équipement ; son éventuel palier n’est pas encore choisi. |
|
||||
| Cosmétique | Équiper une création d’artiste 3D dans un slot au-dessus de la cape, après déblocage ou obtention dans un loot cosmétique. | Apparence uniquement : aucun pouvoir, bonus de statistiques ou slot de stockage accordé par cet objet. |
|
||||
| Familiers | Équiper un spawn egg pour obtenir un effet passif et un effet actif déclenché par une touche. | Pouvoir d’un œuf équipé, apparition d’une entité et portage d’un animal sont des systèmes différents. |
|
||||
|
||||
La progression personnelle reste distincte des acquis communs. Le donjon majeur
|
||||
stabilise les portails des Cavernes pour tout le serveur, y compris les futurs
|
||||
arrivants. Une bibliothèque de plans, un waypoint ou une information nutritionnelle
|
||||
ne deviennent pas collectifs par défaut : leur portée sera écrite dans chaque
|
||||
fiche de capacité. L’inventaire garde sa progression prévue et les prestiges
|
||||
leurs charges personnelles à dépenser dans les slots de faction.
|
||||
|
||||
**Nature’s Compass et la recherche automatique de biomes sont retirés de la
|
||||
sélection actuelle.** L’auteur privilégie l’exploration d’îles aux climats et
|
||||
biomes définis. Cela conserve la minimap et les waypoints, sans les assimiler
|
||||
à une recherche de ressources ou de destinations cachées.
|
||||
|
||||
Les waystones d’ascenseur permettent d’apprendre le geste dans une installation :
|
||||
sauter pour monter, se baisser pour descendre, sans devoir connaître au préalable
|
||||
l’étage adjacent. Les liaisons horizontales restent précises, dans une même
|
||||
dimension et vers une waystone connue, avec un coût d’XP croissant selon la
|
||||
distance. Aucune gratuité ni nouveau tarif vertical n’est fixé ici.
|
||||
|
||||
## Un contrat de déblocage commun
|
||||
|
||||
Proposition : chaque capacité relie une condition de découverte ou un advancement,
|
||||
un éventuel achat en XP et une fonction disponible. Un achat n’est pas imposé
|
||||
à toutes les récompenses. Pour chaque fiche, préciser :
|
||||
|
||||
- ce qui est obtenu et comment le joueur le découvre ;
|
||||
- si l’acquis est personnel ou commun au serveur ;
|
||||
- ce qui persiste et ce que le joueur peut ensuite activer ou masquer ;
|
||||
- ce que fournissent Sanctuary et l’intégration communautaire choisie.
|
||||
|
||||
Le serveur conserve les acquis et contrôle les actions qui engagent le monde :
|
||||
achats, déplacements, effets actifs, transfert d’objets et éventuelle aide de pose.
|
||||
Le client reçoit les capacités pour présenter les bons boutons et données.
|
||||
Masquer JEI ou une minimap dans le pack ne prouve pas qu’un autre client ne puisse
|
||||
afficher une information déjà reçue. Le contrat doit porter sur les possibilités
|
||||
du jeu, sans confondre bouton caché et autorisation côté serveur.
|
||||
|
||||
Le partage de savoir reste possible : un joueur peut expliquer une recette ou
|
||||
montrer une construction à un autre. Un plan débloqué ouvre l’accès à un outil
|
||||
et à sa fiche ; il n’interdit pas de reproduire manuellement une forme observée.
|
||||
|
||||
Le suivi demandé s’étend à un [journal des transformations](journal-et-progression-du-monde.md) :
|
||||
entrées, sorties, procédés et suites d’actions datées. Il alimentera propositions
|
||||
de plans, Backrooms, Indoors et donjons en lien avec la progression réelle.
|
||||
Les compteurs journaliers actuels ne fournissent pas encore cette histoire causale.
|
||||
|
||||
## Pages culinaires et lieux encore inexplorés
|
||||
|
||||
**Direction retenue :** la cuisine d’**It’s Alive !** participe à la progression
|
||||
par des pages à débloquer. La vision prévoit le livre de Kai à reconstituer
|
||||
et des pages secrètes gardées par les sorcières, notamment pour des techniques
|
||||
de transformation. Le déblocage des pages s’articule aux recettes consultables
|
||||
et aux aliments issus de cultures localisées ; connaître une recette ne fournit
|
||||
pas ses ingrédients ni son équipement de cuisine.
|
||||
|
||||
**Kai est le cuisinier fixe de Sanctuary.** Le tirage de conception du
|
||||
11 septembre 2026 a choisi Kai parmi Ari, Efe, Kai, Makena, Noor, Sunny et Zuri.
|
||||
L’auteur a retenu un nom unique pour le lore, sans nouveau tirage par monde.
|
||||
Mojang a ajouté ces **sept** personnages après Steve et Alex ; le rôle culinaire
|
||||
est une création Sanctuary, pas une biographie officielle. Le livre change
|
||||
d’auteur ; la présence de Kai comme visiteur est désormais souhaitée et reste
|
||||
à implémenter dans le système de visites décrit ci-dessous.
|
||||
[Présentation officielle](https://www.minecraft.net/en-us/article/introducing-new-default-skins).
|
||||
|
||||
Un parcours proposé serait : trouver une page, apprendre le procédé, obtenir
|
||||
les ingrédients dans les biomes concernés, construire les équipements et produire.
|
||||
Consommation de la page à la lecture, partage entre joueurs, doublons, portée
|
||||
des acquis et verrouillage éventuel de la préparation restent à définir. Ce
|
||||
contrat doit appartenir à l’intégration Sanctuary, sans imposer cette progression
|
||||
à une partie qui utilise It’s Alive ! seul.
|
||||
|
||||
La révision culinaire demande une **préparation attentive et une récupération
|
||||
au bon moment** : le joueur apprend les temps utiles et observe quand le plat
|
||||
est prêt, notamment dans un chaudron ou un appareil de cuisine. La qualité du
|
||||
résultat doit en dépendre. Fenêtre de réussite, indices visuels ou sonores,
|
||||
sous-cuisson, surcuisson et tolérances restent à concevoir par recette. Le temps
|
||||
réel du calendrier ne fixe pas automatiquement des durées de cuisson de plusieurs heures.
|
||||
|
||||
Une pastille ou une étoile sur l’item pourrait indiquer cette qualité. L’auteur
|
||||
envisage aussi des variantes d’items ; la proposition technique est de conserver
|
||||
une qualité sur l’objet, avec affichage adapté, pour éviter de multiplier les
|
||||
identifiants de chaque plat. Ce choix et le format de sauvegarde ne sont pas
|
||||
implémentés. Qualité culinaire, rareté d’équipement et couleur des lootboxes sont
|
||||
des informations distinctes. Effets de la qualité, empilement, conservation et
|
||||
comportement après déchargement ou arrêt restent à spécifier.
|
||||
|
||||
Kai pourrait organiser des **compétitions de cuisine** et juger les plats selon
|
||||
des critères de préparation à définir. Les règles du concours indiquent ce qui
|
||||
est évalué et les récompenses possibles. Le lien entre alimentation, fabrication et qualité devra
|
||||
être visible dans le journal des transformations.
|
||||
|
||||
L’auteur souhaite des lieux **présents dans le monde initial mais encore
|
||||
inexplorés**. La direction actuelle distingue :
|
||||
|
||||
- un ou deux îlots suspendus envisagés, riches, avec un coffre à déterrer ; une
|
||||
cascade peut permettre d’y monter, mais elle n’est pas obligatoire et peut
|
||||
s’écouler dans le vide indépendamment de ce qui se trouve dessous ;
|
||||
- des bateaux marchands répartis en périphérie de Sanctuary, première voie pour
|
||||
obtenir des villageois ; le navire amarré proposé précédemment est abandonné ;
|
||||
- des montgolfières détenant des richesses, avec accès et contenu à définir.
|
||||
|
||||
Les temples sont retirés de la génération de base. Ils restent une piste future
|
||||
de structures autour de Sanctuary, avec une inspiration du Nether à préciser,
|
||||
sans implémentation demandée maintenant. Nombre exact, visibilité, accès et
|
||||
répartition des autres sites doivent préserver le départ naturel. Les marchands
|
||||
ne fixent pas encore un système de vente de villageois ni un véhicule pilotable.
|
||||
Pages, plans et programmes peuvent accompagner les lieux selon leur usage ;
|
||||
leurs tables de butin restent à concevoir. Voir la [conception des lieux](structures-conception.md).
|
||||
|
||||
## Architecture : une collection de savoir-faire
|
||||
|
||||
**Direction demandée :** reprendre l’idée d’Architext et constituer une bibliothèque
|
||||
de schematics crédités, consultable dans le **menu Build avec des prévisualisations**.
|
||||
**Minecraft Schematics est la source retenue pour l’instant.** Le menu permet de
|
||||
parcourir la bibliothèque personnelle de plans acquis ; l’accès au catalogue
|
||||
complet du site constitue un stade ultérieur, distinct de ce premier bouton.
|
||||
Les victoires sur des monstres peuvent accorder des plans. L’obtention de blocs
|
||||
qui vont ensemble peut également proposer une architecture : le catalogue relie
|
||||
les découvertes de matériaux à des usages possibles. Les associations entre
|
||||
rencontres, matériaux et architectures, répétitions, doublons et partage restent à définir.
|
||||
L’exploration des installations anciennes peut aussi montrer l’utilité d’un plan,
|
||||
en continuité avec les secrets de Sanctuary Island.
|
||||
|
||||
Proposition : utiliser les possessions attestées dans la progression pour
|
||||
reconnaître une famille de matériaux et proposer un plan une fois. La possession
|
||||
simultanée de tous les blocs, le seuil nécessaire et le passage de proposition
|
||||
à déblocage restent à choisir. Le simple fait d’apercevoir un bloc ne remplace
|
||||
pas automatiquement son obtention ; le stock présent et la connaissance acquise
|
||||
restent deux mesures distinctes.
|
||||
|
||||
**Faculté galactique de Build :** après avoir atteint le maximum de **toutes
|
||||
les compétences du personnage**, un achat important en XP ouvre le catalogue
|
||||
des créations du site dans le menu. **64 niveaux** est le coût proposé par
|
||||
l’auteur, à éprouver. Il s’agit
|
||||
d’un accès élargi aux plans des autres créateurs, distinct des plans gagnés au
|
||||
fil de la progression. Cela ne débloque pas automatiquement toutes les autres
|
||||
capacités et ne transforme pas les constructions en blocs gratuits.
|
||||
|
||||
Les prévisualisations doivent aider à choisir une création : vue du build,
|
||||
auteur, dimensions, matériaux et format. Leur technique de rendu reste à choisir.
|
||||
Le catalogue complet est un objectif de consultation ; son index, sa mise à jour
|
||||
et la récupération des fichiers restent à concevoir avec le site. Une fiche
|
||||
peut être visible sans que son format ou sa disponibilité permette déjà son
|
||||
chargement dans Sanctuary. Aucune aspiration du site n’est lancée dans ce chantier.
|
||||
|
||||
Le premier usage proposé est un aperçu de construction, une liste de matériaux,
|
||||
une orientation et une aide à la pose en survie. Débloquer un aqueduc ne le pose
|
||||
pas instantanément dans le monde et ne livre pas ses matériaux. Une construction
|
||||
automatique demanderait une capacité distincte ; elle n’est pas décidée ici.
|
||||
|
||||
Chaque entrée doit garder le titre, l’auteur de la construction, la page source,
|
||||
la version et le format du fichier, les matériaux, les dimensions et le déblocage
|
||||
Sanctuary associé. Si une autre personne a réalisé l’adaptation, son crédit
|
||||
s’ajoute à celui de l’auteur. Les plans de progression forment une collection
|
||||
sélectionnée et vérifiée ; le stade galactique élargit ensuite la consultation
|
||||
au catalogue du site. Une récompense n’entraîne pas un téléchargement aléatoire.
|
||||
|
||||
Les conditions de réutilisation et de redistribution sont vérifiées pour chaque
|
||||
plan retenu ; un téléchargement public et une mention d’auteur ne suffisent pas
|
||||
à établir ces conditions. Pour l’instant, la recherche de catalogue n’importe
|
||||
aucun schematic dans le pack. Le format et les blocs d’un plan doivent également
|
||||
être compatibles avec le jeu réellement distribué.
|
||||
|
||||
### Catalogues repérés
|
||||
|
||||
La recherche avait repéré deux sources ; **Minecraft Schematics est désormais
|
||||
retenu**, Abfielder reste une référence écartée de cette première intégration.
|
||||
Aucune bibliothèque n’est déjà importée.
|
||||
|
||||
| Catalogue | Exemple et limites |
|
||||
| --- | --- |
|
||||
| [Minecraft Schematics](https://www.minecraft-schematics.com/) — retenu | Catalogue de schémas et de mondes. Exemple : [Bakery Market Stall, EcoSMP_Official](https://www.minecraft-schematics.com/schematic/31569/). Le format du fichier et le rôle du contributeur sont à vérifier : publier une fiche ne prouve pas que l’on est l’auteur original. |
|
||||
| [Abfielder](https://abfielder.com/Products/BrowseProducts.php) — référence non retenue | Autre source repérée pendant la recherche ; pas d’intégration prévue à ce stade. |
|
||||
|
||||
Les [conditions de Minecraft Schematics](https://www.minecraft-schematics.com/terms/)
|
||||
et celles d’[Abfielder](https://www.abfielder.com/termsAndConditions) ne donnent
|
||||
pas une autorisation globale de reprendre toutes les créations dans le pack.
|
||||
Le choix d’un plan pour la bibliothèque ne l’autorise pas non plus automatiquement
|
||||
comme structure générée dans le monde.
|
||||
|
||||
**Architext : référence historique non retrouvée dans la recherche actuelle.**
|
||||
Les sources, documents, noms de fichiers et chemins de l’historique Git 26.2 ont
|
||||
été consultés, avec une recherche bornée également sous le dossier Sanctuary.
|
||||
La reprise est donc celle de l’intention exprimée par l’auteur ; aucun ancien
|
||||
comportement de placement ou de progression n’est attesté ici. L’auteur prévoit
|
||||
de retrouver le module ; son étude sera ajoutée lorsqu’il sera disponible.
|
||||
|
||||
## Inventaire, équipement et familiers
|
||||
|
||||
**Décision demandée : réimplémenter la manipulation d’inventaire dans Sanctuary.**
|
||||
Glisser, répartir et transférer les stacks devront suivre les emplacements ouverts
|
||||
de l’inventaire incrémental. Ne pas intégrer Mouse Tweaks comme base imposée ni
|
||||
confier cette logique à des profils de mods communautaires. Les préférences
|
||||
d’affichage et de commandes sont à distinguer du nombre de slots autorisés.
|
||||
|
||||
La direction ajoute des emplacements d’équipement pour cape et familier, avec
|
||||
le slot de spawn egg près de la main secondaire, au-dessus dans la disposition
|
||||
évoquée. Elle ne fixe pas encore plusieurs familiers simultanés ni des rangées
|
||||
de stockage supplémentaires accordées par une cape. La cape conserve pour
|
||||
l’instant son rôle d’apparence ; ses éventuels effets restent à choisir.
|
||||
|
||||
Un **slot cosmétique distinct au-dessus de la cape** est désormais demandé.
|
||||
Il reçoit des créations 3D confiées à un artiste et débloquées en jeu, notamment
|
||||
par des loots cosmétiques. Ce slot et son équipement sont purement visuels.
|
||||
Il ne remplace ni le familier ni la cape et n’ajoute aucun pouvoir. La coexistence
|
||||
des modèles avec l’armure et la cape, les emplacements visuels et la collection
|
||||
restent à dessiner.
|
||||
|
||||
La cible est un **passif et un actif pour chaque spawn egg du catalogue Sanctuary**.
|
||||
La touche d’activation sera configurable. Effet, durée, coût, délai de réutilisation,
|
||||
cible et interactions doivent être décrits ensemble. Les règles d’acquisition,
|
||||
de retrait, de mort du joueur et de changement d’œuf restent à définir pour la
|
||||
nouvelle implémentation ; les comportements de l’ancien mod ne sont pas repris
|
||||
automatiquement. **La priorité de réécriture est leur présence dans le monde** :
|
||||
collisions avec le décor, déplacements capables de monter et descendre les
|
||||
obstacles, sans collision gênant les joueurs. Les anciens usages de pouvoirs
|
||||
restent des références à réévaluer.
|
||||
|
||||
Un familier ne donne **aucun loot**. L’auteur évoque des familiers tuables, puis
|
||||
laisse cette possibilité en suspens : leur mortalité n’est pas décidée. Défaite,
|
||||
retour éventuel et rapport aux pouvoirs de l’œuf restent à concevoir ensemble.
|
||||
|
||||
Leur collection doit aussi avoir des raretés : commun, peu commun, rare,
|
||||
extraordinaire, puis légendaire ou galactique selon la hiérarchie à préciser.
|
||||
**L’Ender Dragon et le Wither sont des familiers galactiques** dans la direction
|
||||
retenue. La place relative de légendaire et galactique reste ouverte ; ce
|
||||
classement ne décide pas de la taille physique ni de la puissance de leurs effets.
|
||||
|
||||
### Lecture ciblée des anciens familiers
|
||||
|
||||
Le dépôt voisin **26.2** a été consulté sans exécution ni modification. Sous
|
||||
`sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/` :
|
||||
|
||||
| Référence locale | Comportement observé et enseignement |
|
||||
| --- | --- |
|
||||
| `mixin/client/InventoryScreenMixin.java` et `gameplay/SanctuaryCompanionEggSlot.java` | Slot d’œuf unique au-dessus de la main secondaire, cape séparée ; le slot accepte un spawn egg, quantité un. Cette disposition fournit une référence à redessiner avec le nouvel inventaire. |
|
||||
| `gameplay/SanctuaryCompanionPowers.java` | Le serveur relit l’œuf et valide le déblocage et le délai avant l’actif. Les passifs incluent chauve-souris : vision nocturne/chute lente, avec sonar actif ; axolotl : respiration aquatique, avec soin actif. Ces effets historiques ne fixent pas le nouvel équilibrage. |
|
||||
| `progression/SanctuaryPlayerProgress.java` et `gameplay/SanctuaryInventoryCapacity.java` | Œuf et cape enregistrés par joueur ; objets perdus à la mort sans keepInventory. Ce contrat historique n’arrête pas celui de la future réécriture. |
|
||||
| `gameplay/SanctuaryCompanions.java` et `gameplay/SanctuaryCompanionPowers.java` | Le familier visible est une copie temporaire distincte des pouvoirs de l’œuf. Sa mort ne coupe pas automatiquement les passifs ; l’état vaincu et les délais d’actifs ne sont pas persistés. La nouvelle version doit définir leur devenir à la mort et à la reconnexion. |
|
||||
| `gameplay/SanctuaryCapeService.java` | Cape cosmétique et synchronisée entre joueurs ; aucune augmentation de capacité retrouvée dans ce système. |
|
||||
|
||||
Les œufs acceptés par le slot dépassent les identifiants dotés d’un pouvoir
|
||||
explicite : tous les œufs n’ont donc pas effectivement un couple actif/passif
|
||||
dans l’ancien code. La nouvelle version doit rendre lisible la prise en charge
|
||||
de chaque œuf et éviter les objets équipables sans effet défini. Le familier
|
||||
visible neutralisé par l’ancien système ne doit pas servir tel quel au portage
|
||||
d’un hostile qui reste dangereux.
|
||||
|
||||
## Porter un animal sans le neutraliser
|
||||
|
||||
Le geste retenu est **sneak + clic droit**, pour porter un animal sur la tête,
|
||||
avec un seul mob porté à la fois selon la vision existante. La nouvelle direction
|
||||
permet aussi de porter des mobs hostiles, **qui restent capables d’attaquer**.
|
||||
Porter ne doit donc pas les transformer automatiquement en animaux apprivoisés
|
||||
ou supprimer leur danger. Tailles extrêmes, boss, conflits d’interaction et
|
||||
conditions de dépose restent à concevoir.
|
||||
|
||||
Carry On est une référence d’interaction, pas le contrat déjà adopté. Sa page
|
||||
décrit le transport de blocs à inventaire et de mobs ; le portage d’hostiles en
|
||||
survie est désactivé par défaut et configurable. Elle ne suffit pas à établir
|
||||
le comportement de combat demandé pendant le portage.
|
||||
[Page du projet Carry On](https://www.curseforge.com/minecraft/mc-mods/carry-on).
|
||||
La capacité de déplacer tous les coffres, spawners ou machines n’est pas déduite
|
||||
de cette référence. Elle aurait des conséquences propres pour les boutiques et
|
||||
les donjons, à décider avant intégration.
|
||||
|
||||
### Bateau-poule et extraction aérienne
|
||||
|
||||
**Montage retenu :** placer un villageois ou un animal dans un bateau, relier
|
||||
ce bateau par une corde au bateau-poule, puis le soulever et le transporter.
|
||||
Le bateau occupé sert de nacelle ; le bateau-poule fournit le vol. Cette
|
||||
combinaison donne un usage aux villageois des navires marchands et aux animaux
|
||||
des laboratoires d’expansion, notamment les Mooblooms.
|
||||
|
||||
La traction doit rester une interaction construite par les joueurs. Nombre de
|
||||
bateaux transportés, charge, vitesse, longueur de corde, dépose, rupture et
|
||||
comportement après déchargement restent à tester. Aucun objet ou passager n’est
|
||||
dupliqué par le transfert ; il s’agit du déplacement des occupants existants.
|
||||
Le montage ne décide pas des autres capacités de portage ou de téléportation.
|
||||
|
||||
### Booat
|
||||
|
||||
**Booat est le nom en jeu ; « bi-poule » reste le surnom entre nous.** Ce grand
|
||||
bateau où l’on peut être à quatre fait partie des **mécaniques centrales du mod**.
|
||||
**Capacité confirmée : quatre joueurs sur l’eau, puis deux joueurs et deux
|
||||
poules en vol.** Les poules occupent deux des quatre places et fournissent le
|
||||
vol ; le pilote compte parmi les deux joueurs transportés.
|
||||
|
||||
La recette demandée combine **quatre bateaux** et le véhicule doit exister dans
|
||||
toutes les variantes de bois. Disposition de la recette, mélange éventuel des
|
||||
essences et traitement des variantes de radeaux restent à préciser. Le Booat
|
||||
s’ajoute au bateau-poule simple et au biplan déjà envisagé. La traction par corde
|
||||
est une articulation à tester, avec charge et comportement en cas de perte
|
||||
d’une poule à définir ; aucune recette ni entité n’est implémentée ici.
|
||||
|
||||
## Visiteurs et pratiques des anciens
|
||||
|
||||
**Direction actuelle : des personnages visitent Sanctuary pendant une journée**,
|
||||
se promènent sur l’île et ouvrent un menu de discussion au clic droit. Leur
|
||||
activité peut comprendre une quête liée à leur domaine, une compétition ou des
|
||||
offres spécialisées. Le modèle évoqué est celui des visiteurs d’Animal Crossing,
|
||||
avec par exemple un marchand de tapis ou de décoration.
|
||||
|
||||
L’invocation proposée auparavant reste en réserve : la visite est le fonctionnement
|
||||
à concevoir en premier, sans ajouter deux systèmes de venue simultanément.
|
||||
La nature de cette présence demeure mystérieuse ; un ancien visible n’est pas
|
||||
la confirmation d’une résurrection ni l’explication de la disparition des neuf.
|
||||
Les rencontres n’exigent pas une nouvelle ruine ou une maison permanente pour
|
||||
chaque personnage et conservent le départ naturel.
|
||||
|
||||
| Personnage | Domaine | Statut |
|
||||
| --- | --- | --- |
|
||||
| Kai | Cuisine, pages, préparation et concours avec jugement des plats. | Cuisinier fixe retenu ; détails des concours à définir. |
|
||||
| Alex | Nature et biomes. | Rôle retenu ; ne réintroduit pas Nature’s Compass ni une recherche automatique de biomes. |
|
||||
| Ari | Architecture, quêtes de construction, plans et accès au Build galactique. | Rôle retenu ; activités et rencontres à concevoir. |
|
||||
| Makena | Redstone, ordinateurs et automatismes. | Rôle retenu ; ateliers et programmes de Régie à concevoir. |
|
||||
| Zuri | Étoiles, constellations, temps réel et calendrier. | Rôle retenu ; activités de l’observatoire à concevoir. |
|
||||
| Steve | Personnage absent. | Absence retenue ; il ne devient pas un visiteur ordinaire. |
|
||||
|
||||
L’auteur souhaite des **flashs d’Herobrine**. Forme, fréquence et déclenchement
|
||||
restent ouverts ; ces apparitions ne prouvent pas qu’Herobrine est Steve et ne
|
||||
définissent pas un boss. Les rôles d’Efe, Noor et Sunny et l’identité d’un marchand
|
||||
de décoration restent à préciser ; ils ne reçoivent pas automatiquement une boutique.
|
||||
|
||||
Ari pourrait donner des quêtes de bâtiments et incarner l’accès galactique au
|
||||
catalogue. **Toutes les compétences au maximum et coût proposé de 64 niveaux**
|
||||
sont les conditions prévues ; la rencontre ne les annule pas. Les ordinateurs
|
||||
restent étudiables dans de grandes salles de farming **sans panneaux explicatifs** :
|
||||
le visiteur peut proposer une activité sans remplacer cette découverte par un cours.
|
||||
|
||||
La journée de présence se rattache au calendrier réel du serveur. Fréquence des
|
||||
visites, heures d’arrivée/départ ou durée de vingt-quatre heures, nombre simultané
|
||||
et devenir d’une quête après le départ restent à décider. Les conversations,
|
||||
offres et récompenses demandent leurs propres règles ; aucun achat ni dialogue
|
||||
n’est ajouté par ce document. Les échanges commerciaux doivent s’articuler à
|
||||
l’économie en rubis et à la progression, sans offrir à chaque visite tous les
|
||||
biens recherchés par les joueurs.
|
||||
|
||||
## Raids collectifs instanciés
|
||||
|
||||
**Direction retenue : un raid pour tout le serveur par semaine.** Cette limite
|
||||
est commune ; elle n’accorde pas un raid supplémentaire à chaque joueur.
|
||||
**Dix joueurs doivent se réunir à un portail secret situé sur une île obtenue
|
||||
par génération d’expansion.**
|
||||
Le raid difficile est conçu pour cet effectif ; il ne devient pas plus facile
|
||||
parce que seulement deux, trois ou quatre joueurs se présentent. Ce portail
|
||||
constitue un lieu distinct du laboratoire, donjon d’accès aux Cavernes
|
||||
qui reste sur Sanctuary Island. Les conditions de découverte et d’activation
|
||||
du portail, ainsi que la fréquence ou la garantie de génération d’une île
|
||||
d’accès aux raids, restent à définir. Aucun portail n’est exigé sur chaque
|
||||
expansion. Le découvrir n’accorde pas de raid supplémentaire au-delà du quota
|
||||
hebdomadaire du serveur. La gestion d’une déconnexion reste également à préciser.
|
||||
|
||||
Le portail conduit le groupe dans un **donjon généré procéduralement**, au sein
|
||||
d’une instance préparée pour l’épreuve. Les **Trial Chambers** servent
|
||||
d’inspiration pour la conception de ce lieu, avec une palette propre encore à
|
||||
choisir ; cela ne fixe ni une reprise de leur code ni leurs mécaniques exactes.
|
||||
Il se joue **en mode aventure, avec des blocs incassables et sans sortie libre
|
||||
pendant l’épreuve**. Ce contrat demande de traiter aussi les effets des explosions,
|
||||
des objets et des téléportations Sanctuary ; le seul nom du mode de jeu ne
|
||||
constitue pas sa vérification. Les parcours, objectifs et mécanismes de résolution
|
||||
doivent rester praticables dans ces conditions. Les règles d’abandon et de retour
|
||||
sûr restent à concevoir : ce choix ne condamne pas un joueur à rester bloqué dans
|
||||
l’instance après un incident.
|
||||
|
||||
La progression sert à préparer les nouveaux donjons : rencontres, organisation,
|
||||
contraintes et objectifs évoluent, au-delà d’une augmentation des statistiques.
|
||||
La carte, son effectif de référence, ses règles et sa difficulté sont fixés au
|
||||
lancement ; une reconnexion reprend la même tentative. La fin de l’épreuve doit
|
||||
restaurer les conditions habituelles des joueurs. Mort, échec collectif, abandon,
|
||||
reprise après incident et retour sûr restent à définir avant implémentation.
|
||||
|
||||
Les cartes créées par les joueurs restent une **possibilité future**. Il faudrait
|
||||
distinguer leur atelier de construction et la version admise dans le circuit
|
||||
récompensé. Le butin de raid appartient aux règles du serveur ; dessiner un coffre
|
||||
ou déclarer une difficulté ne donne pas automatiquement droit à son contenu.
|
||||
Le mode de sélection et de validation de telles cartes reste à concevoir,
|
||||
sans restreindre ici la création libre.
|
||||
|
||||
Le raid fournit un **butin commun matériel à ramasser, dimensionné pour dix
|
||||
joueurs**, pouvant comprendre des équipements enchantés différents et du titane.
|
||||
Les participants le répartissent librement entre eux ; le serveur ne crée pas
|
||||
d’attribution personnelle ni de partage automatique à parts égales. Les quantités,
|
||||
la qualité, les conditions d’apparition et le devenir du butin lors d’une
|
||||
déconnexion ou d’un incident restent à définir. Une réentrée dans la même instance
|
||||
ne crée ni nouvelle tentative ni nouvelle réserve de récompenses.
|
||||
|
||||
Le moment qui consomme le raid hebdomadaire reste ouvert : entrée effective,
|
||||
lancement ou autre étape confirmée. Il faut aussi choisir le jour de remise à
|
||||
disposition, la semaine calendaire ou le délai glissant et le traitement d’un
|
||||
incident. Le calendrier du jeu fournit le cadre de temps réel ; aucun jour de
|
||||
raid n’est fixé par la loterie du dimanche. Ces raids restent distincts du
|
||||
donjon initial qui ouvre collectivement les Cavernes, des tunnels explorables
|
||||
et des spawners exploitables dans les donjons ordinaires.
|
||||
|
||||
## Familles de butin et rareté des équipements
|
||||
|
||||
La famille d’un loot décrit son contenu ; la couleur décrit la rareté d’un
|
||||
équipement. Un cosmétique rare reste sans pouvoir. La nouvelle sélection retient :
|
||||
|
||||
| Famille | Contenu envisagé |
|
||||
| --- | --- |
|
||||
| Général | Objets ordinaires ou intéressants et équipements de plusieurs raretés ; cas extrêmement rares de légendaire. |
|
||||
| Familiers / spawn eggs | Œufs de familiers. L’auteur dit « loots spawner » ; la correspondance avec les spawn eggs de la vision est l’hypothèse actuelle, sans attribuer de blocs spawners. |
|
||||
| Capes | Éléments de la collection de capes. |
|
||||
| Cosmétiques | Créations visuelles équipables dans le nouveau slot, sans bonus de jeu. |
|
||||
|
||||
Les couleurs évoquées sont blanc, vert, bleu, violet et jaune. Proposition de
|
||||
classement à valider : **commun, peu commun, rare, épique, légendaire** dans cet
|
||||
ordre. Matériau, enchantements, durabilité et rareté ne sont pas une même donnée.
|
||||
Les légendaires sont très puissants mais peuvent rester cassables.
|
||||
|
||||
**Anomaly** est le niveau ultime : équipement en titane, doté d’enchantements
|
||||
exceptionnels, incassable, avec un nom écrit en galactique. Cette direction
|
||||
prolonge les équipements infrabricables de la vision ; elle ne rend pas toutes
|
||||
les machines fabriquées en titane incassables ou Anomaly. Les bornes des effets
|
||||
et les combinaisons d’enchantements restent à équilibrer.
|
||||
|
||||
L’ancienne lootbox Anomaly et sa place dans la loterie sont conservées comme
|
||||
pistes à articuler à cette nouvelle classification. Le caractère ultime de
|
||||
l’équipement ne décide pas seul de son contenant ni de toutes ses sources.
|
||||
Le butin général peut exceptionnellement donner du légendaire ; sa probabilité
|
||||
et l’éventuelle présence d’Anomalies ne sont pas fixées. Le butin commun d’un raid
|
||||
ne garantit ni une Anomaly au groupe ni un équipement de ce niveau à chaque
|
||||
participant.
|
||||
|
||||
Les probabilités doivent être évaluées sur le volume réel d’ouvertures issu des
|
||||
farms : le nombre moyen de résultats rares dépend du nombre d’essais autant que
|
||||
de la chance par essai. Les farms ordinaires restent possibles ; la rareté des
|
||||
récompenses du raid s’appuie aussi sur son rythme collectif hebdomadaire.
|
||||
Tables, pondérations, doublons, conditions de gains et circulation des objets
|
||||
restent à spécifier. Aucun taux chiffré n’est adopté ici.
|
||||
|
||||
## Références et intégration du pack
|
||||
|
||||
Sources consultées le **11 septembre 2026**. Elles décrivent les projets ; elles
|
||||
ne valident pas à elles seules un ensemble compatible avec 26.3-pre-2.
|
||||
|
||||
| Projet | Fait vérifié et conséquence pour Sanctuary |
|
||||
| --- | --- |
|
||||
| [JEI — API de recettes](https://github.com/mezz/JustEnoughItems/blob/26.2/CommonApi/src/main/java/mezz/jei/api/recipe/IRecipeManager.java) | Expose le masquage et le réaffichage des recettes pour la progression. Un adaptateur pourrait refléter les acquis Sanctuary ; cette API d’affichage ne bloque pas la fabrication. La référence lue est une branche 26.2, pas une preuve de compatibilité 26.3. |
|
||||
| [Xaero’s Minimap](https://www.curseforge.com/minecraft/mc-mods/xaeros-minimap) | Décrit des réglages serveur, une exigence d’objet et des effets désactivant minimap, waypoints, radar d’entités ou cartes de cavernes. Ces points de raccord sont à évaluer pour les paliers. La téléportation par waypoint reste soumise aux permissions de commande. |
|
||||
| [Litematica](https://modrinth.com/mod/litematica) et [MaLiLib](https://github.com/maruohon/malilib) | Plans fantômes, listes de matériaux et vérification de construction, avec bibliothèque d’interface/configuration. Le catalogue Sanctuary serait un ajout ; l’aide de pose doit être essayée avec le serveur, notamment son [protocole Easy Place](https://github.com/maruohon/litematica/wiki/Easy-Place). |
|
||||
| [AppleSkin](https://github.com/squeek502/AppleSkin) | Affiche faim, saturation, épuisement et effet potentiel des aliments. Évaluer son intégration avec le HUD à capacité variable et les textures Sanctuary, ou développer ce rendu nativement. Ne pas transposer l’API Forge à Fabric sans vérification. |
|
||||
| [Nature’s Compass](https://github.com/MattCzyr/NaturesCompass) — référence retirée | Le projet permet une recherche de biomes ; cette fonction n’est plus retenue pour la progression actuelle de Sanctuary. |
|
||||
| [Carry On](https://www.curseforge.com/minecraft/mc-mods/carry-on) | Référence pour le geste de transport ; la règle des hostiles et les interactions Sanctuary nécessitent leur propre validation. |
|
||||
| [Sound Physics Remastered](https://github.com/henkelmax/sound-physics-remastered) | Acoustique spatiale ; préférence optionnelle proposée, avec vérification de son effet sur les sons utiles au jeu. |
|
||||
| [NetherPortalFix](https://github.com/TwelveIterations/NetherPortalFix) | Corrige les destinations de retour des portails du Nether en multijoueur. Candidat pour le pack, sans déblocage de progression ; ne garantit pas tous les futurs portails Galactium, Indoors ou Cavernes. |
|
||||
|
||||
Les catalogues Modrinth de [JEI](https://modrinth.com/mod/jei/versions),
|
||||
[Xaero](https://modrinth.com/mod/xaeros-minimap/versions),
|
||||
[Litematica](https://modrinth.com/mod/litematica/versions) et
|
||||
[MaLiLib](https://modrinth.com/mod/malilib/versions), interrogés pour Fabric et
|
||||
exactement `26.3-pre-2`, n’ont retourné aucun fichier déclaré pour cette combinaison
|
||||
au relevé. Leur disponibilité sur la cible est **non confirmée**, sans conclure
|
||||
à une incompatibilité. Les autres candidats ne sont pas validés pour cette cible
|
||||
par la seule lecture de leurs descriptions.
|
||||
|
||||
Aucun JAR, dépendance ou configuration de profil n’est ajouté ici. Pour chaque
|
||||
intégration future, vérifier l’artefact Fabric exact, ses dépendances et ses
|
||||
points de raccord, puis essayer les états avant/après déblocage. L’installation
|
||||
d’un mod dans le pack et l’obtention d’une capacité par un joueur sont deux
|
||||
opérations différentes ; on n’installe pas un JAR au moment d’un advancement.
|
||||
|
||||
## Prochain résultat de conception
|
||||
|
||||
Décrire un petit ensemble de fiches jouables reliant une découverte, un acquis
|
||||
et son usage concret, puis choisir les premiers plans et effets de familiers.
|
||||
Les coûts, paliers, sources des plans et effets exacts restent en discussion.
|
||||
Ce cahier ne lance ni portage des anciens modules ni livraison du pack.
|
||||
@@ -1,119 +0,0 @@
|
||||
# Archives de Sanctuary : dater le projet et ses versions
|
||||
|
||||
Audit documentaire du **11 septembre 2026**, en lecture seule. Les souvenirs
|
||||
initiaux de l’auteur sont : création le 16 mars, développement le 24 mai,
|
||||
alpha le 17 août et bêta le 8 septembre 2026. Il a précisé que ces dates étaient
|
||||
approximatives. On conserve ces indications et on les confronte aux traces.
|
||||
|
||||
**Résultat le plus solide : une distribution classée alpha est publiée sur
|
||||
Modrinth le 7 juillet 2026. L’archive locale est exactement celle de cette
|
||||
publication.** Le 17 août ne peut donc pas être présenté comme la première
|
||||
publication alpha de toutes les lignées de Sanctuary.
|
||||
|
||||
## Chronologie retrouvée
|
||||
|
||||
Les heures locales ci-dessous sont exprimées en **Europe/Paris** ; les
|
||||
horodatages des services sont conservés en UTC dans les références.
|
||||
|
||||
| Date | Trace retrouvée | Ce qu’elle établit |
|
||||
| --- | --- | --- |
|
||||
| **16 mars 2026** | Souvenir de création donné par l’auteur. | Origine possible du projet. Aucune pièce ciblée consultée ne confirme encore ce jour. |
|
||||
| **24 mai 2026** | Ancien site : article `Release 1.0.0`, qualifié de première version publique, et aperçu de l’île. | Date éditoriale explicitement écrite ; les fichiers ne sont conservés dans Git qu’à partir de juillet. Elle ne prouve pas seule une publication en mai. |
|
||||
| **21 juin 2026, 14:01** | Champ `published` du projet Modrinth `FgOqyAXd`. | Repère de publication du projet sur cette plateforme ; pas la date de création de Sanctuary ni d’une version précise. |
|
||||
| **4 juillet 2026, 20:27** | Premier commit du dépôt racine, sources déjà à Minecraft `26.1.2`, mod `1.0.0`. | État de sources conservé. L’import initial n’est pas le premier jour de travail. |
|
||||
| **7 juillet 2026, 16:31** | Modrinth : `Sanctuary v2026.07.07`, canal **alpha**, Minecraft `26.1.2`. | Publication d’une version datée par la plateforme ; archive retrouvée localement et identique. |
|
||||
| **7 juillet 2026, 21:44** | Premier commit du site, contenant déjà les articles datés du 24 mai. | Les mentions du 24 mai sont présentes dans cet état de juillet. Elles ne constituent pas une archive Git de mai. |
|
||||
| **8 juillet 2026, 05:27** | Ajout de `sanctuary.mrpack` dans le dépôt racine. | Conservation Git de l’artefact publié la veille, sans changement de sa date de publication. |
|
||||
| **17 août 2026, 09:57** | Création Gitea du dépôt `koka/sanctuary`. | Confirme ce jour pour l’ouverture de ce dépôt ; ne suffit pas à dater la première alpha 26.2. |
|
||||
| **25 août 2026, 22:20** | Import initial de la lignée 26.2, déjà numérotée `26.2.0-alpha.195`. | Les versions précédentes sont antérieures à cet import, mais toutes leurs dates ne sont pas conservées dans cet historique. |
|
||||
| **8 septembre 2026, 00:53 puis 03:24** | Création Gitea de `sanctuary-beta`, puis premier commit Fabric 26.3, version `0.1.0-alpha.1`. | Nouvelle base du projet nommé Beta ; sa version initiale reste explicitement alpha. |
|
||||
| **10 septembre 2026** | Ancienne lignée 26.2 à `alpha.244` ; nouvelle lignée distribuée à `0.1.0-alpha.22`. | Les lignées coexistent. Le nom `sanctuary-beta` n’est pas une migration rétroactive de leur phase. |
|
||||
|
||||
## Preuves locales et sources primaires
|
||||
|
||||
### Le site et les sources de juillet
|
||||
|
||||
- [Article du site, `blogPosts.js:8`](../../sanctuary-minecraft.fr/src/blogPosts.js#L8) : date `2026-05-24`, version `1.0.0`. La seconde entrée porte la même date. Les changements décrivent aussi le site ; un texte préparatoire demeure possible.
|
||||
- Dépôt `sanctuary-minecraft.fr`, commit **`6a24b3f92bc4af5f76fff78d1c97bcb8dcb4926e`**, auteur `2026-07-07T21:26:28+02:00`, enregistrement `2026-07-07T21:44:11+02:00`. Les deux articles sont déjà présents.
|
||||
- Dépôt racine `sanctuary`, commit **`c796a203218a8308f485b9eeb0d95233563a6c4b`**, `2026-07-04T20:27:08+02:00`, intitulé « Initial sanctuary sources ». Son fichier `Worldgen/sanctuary/gradle.properties` contient `minecraft_version=26.1.2` et `mod_version=1.0.0`.
|
||||
- Dans ce dépôt, commit **`4dcb80ec9746cde95a28c75cb946341c3df5b463`**, `2026-07-08T05:27:00+02:00`, conservation du pack.
|
||||
|
||||
Une date auteur/committer est une métadonnée Git, pas une certification externe
|
||||
du jour de sortie. Les noms et versions sont retranscrits tels qu’ils existent.
|
||||
|
||||
### La publication Modrinth et le pack conservé
|
||||
|
||||
[Projet Sanctuary](https://modrinth.com/modpack/sanctuary-koka99),
|
||||
[version retrouvée](https://modrinth.com/modpack/sanctuary-koka99/version/wieO1Ib6),
|
||||
[métadonnées du projet](https://api.modrinth.com/v2/project/FgOqyAXd),
|
||||
[métadonnées de la version](https://api.modrinth.com/v2/version/wieO1Ib6).
|
||||
|
||||
- Projet : `published = 2026-06-21T12:01:28.666880Z`.
|
||||
- Version `wieO1Ib6` : `version_number = 2026.07.07`, `version_type = alpha`, `date_published = 2026-07-07T14:31:33.057373Z`.
|
||||
- Fichier `sanctuary.mrpack`, **7 221 067 octets**. Son SHA-512 annoncé par la plateforme est identique à celui de [l’archive locale](../../sanctuary.mrpack).
|
||||
- Dans `modrinth.index.json` : `versionId = 07.07.2026`, Minecraft `26.1.2`, Fabric Loader `0.19.2`.
|
||||
- SHA-256 local : `42662167aad9f0b58bea06bda2c1e482d6618a55abab07a8120fa517a45b0909`.
|
||||
|
||||
La vérification lit uniquement le manifeste de l’archive et calcule ses
|
||||
empreintes ; elle n’installe ni ne lance le pack. Une seule version est renvoyée
|
||||
par l’API publique du projet au relevé. Cela ne prouve pas qu’il n’y en ait jamais
|
||||
eu d’autres, retirées ou diffusées ailleurs.
|
||||
|
||||
### Les lignées 26.2 et 26.3
|
||||
|
||||
[Métadonnées Gitea du dépôt historique](https://git.botsu.net/api/v1/repos/koka/sanctuary)
|
||||
: `created_at = 2026-08-17T07:57:49Z`. Les listes publiques de tags et de
|
||||
releases de ce dépôt sont vides au relevé ; des branches de livraison existent.
|
||||
|
||||
Le [changelog 26.2 importé](https://git.botsu.net/koka/sanctuary/src/commit/94644a28f05b8010f06ec1181fe6c74a50ad80d6/CHANGELOG.md)
|
||||
conserve `26.2.0-alpha.1` et décrit dix modules, une instance Prism et un premier
|
||||
MRpack, **sans date**. Son introduction précise que cet historique est reconstitué
|
||||
depuis les archives et que les dates disponibles ne sont pas fiables.
|
||||
Le commit d’import **`94644a28f05b8010f06ec1181fe6c74a50ad80d6`**, le
|
||||
25 août à 22:20:18 +02:00, contient déjà le **pack `26.2.0-alpha.195`** et le
|
||||
**module Sanctuary `0.0.0-alpha.111`** : les deux numérotations sont distinctes.
|
||||
Les 263 commits alors atteignables depuis `main` ne portent aucune date auteur
|
||||
ou committer antérieure au 25 août ; les autres branches ne sont pas toutes
|
||||
auditées. Le commit de livraison
|
||||
**`72f609f77cd59b074ff8b77f7161fcb89bdba4eb`**, le 10 septembre à
|
||||
14:45:33 +02:00, est encore une alpha 26.2.
|
||||
|
||||
[Métadonnées du dépôt actuel](https://git.botsu.net/api/v1/repos/koka/sanctuary-beta)
|
||||
: `created_at = 2026-09-07T22:53:39Z`, soit le **8 septembre à 00:53:39**
|
||||
à Paris. Ce décalage explique la différence de date UTC.
|
||||
|
||||
Premier [commit des sources 26.3](https://git.botsu.net/koka/sanctuary-beta/commit/e48de1b400e8a406c6a6fe91e995f89311f45106)
|
||||
: auteur `2026-09-08T03:24:04+02:00`, commit `03:24:19+02:00` ;
|
||||
`gradle.properties` et `packwiz/pack.toml` donnent `0.1.0-alpha.1`, sur
|
||||
Minecraft `26.3-pre-2`. Les [releases publiques](https://git.botsu.net/api/v1/repos/koka/sanctuary-beta/releases?limit=50)
|
||||
confirment les distributions alpha suivantes, de l’alpha.2 le 8 septembre
|
||||
à l’alpha.22 le 10 septembre au moment du relevé.
|
||||
|
||||
## Conséquences pour la chronologie et le lore
|
||||
|
||||
On distingue maintenant **création du projet**, **première diffusion retrouvée**,
|
||||
**refonte 26.2** et **nouvelle base 26.3**. « Dev → alpha → bêta » ne décrit pas
|
||||
fidèlement tous les numéros retrouvés : une `1.0.0` locale précède déjà les
|
||||
alphas ultérieures. Chaque lignée garde son histoire.
|
||||
|
||||
Le **16 mars** peut rester l’origine narrative souhaitée, si l’auteur la
|
||||
confirme comme telle, même sans document contemporain. Le **24 mai** a une
|
||||
trace éditoriale ; **le 7 juillet est une publication alpha directement
|
||||
recoupée**. Le **17 août** et le **8 septembre** sont attestés pour leurs
|
||||
dépôts respectifs. Aucune de ces dates ne fixe automatiquement la disparition
|
||||
de Steve et des autres anciens.
|
||||
|
||||
Le [calendrier proposé](chronologie-sanctuary.md) garde les calculs fondés sur
|
||||
le 16 mars explicitement conditionnels. Retrouver une version plus ancienne
|
||||
précisera l’histoire documentaire sans réécrire des sauvegardes existantes.
|
||||
|
||||
## Limites de l’audit
|
||||
|
||||
Les recherches portent sur les documents et métadonnées des dépôts locaux
|
||||
pertinents, les archives identifiées, Gitea et Modrinth. Les dates de copie des
|
||||
dossiers et les noms de sauvegardes ne sont pas des preuves de publication.
|
||||
Les worktrees `26.2-notch`, `26.2-terminal-menu` et `26.2-ticket-90` partagent
|
||||
le dépôt 26.2 : ce ne sont pas trois témoignages indépendants. Les mondes n’ont
|
||||
pas été ouverts, les anciens dépôts n’ont pas été modifiés et aucun artefact
|
||||
historique n’a été republié. D’autres sauvegardes ou publications disparues
|
||||
pourraient encore préciser le 16 mars, le 24 mai et le début exact de 26.2.
|
||||
@@ -1,83 +0,0 @@
|
||||
# Minecraft Java, 2009–2014 : chronologie des formes, ressources et systèmes
|
||||
|
||||
Recherche documentaire du 11 septembre 2026. Ce dossier couvre les changements majeurs de contenu de Pre-classic à la 1.8, avec les premières versions de développement utiles. Il ne répertorie pas chaque correctif ni chaque build ancien perdu. Les dates décrivent le développement du logiciel, **pas une histoire canonique des habitants de Minecraft** : rien ici n’attribue les inventions à Steve, Alex ou aux autres personnages de Sanctuary.
|
||||
|
||||
Les versions Alpha, Beta et les versions stables sont des séries différentes : Beta 1.8 n’est pas la 1.8 de 2014. Plusieurs identifiants Indev/Infdev ont été reconstruits à partir de dates et d’archives ; le détail horaire peut changer lorsqu’un exemplaire est retrouvé. Une texture présente dans les fichiers, un objet disponible et une mécanique fonctionnelle constituent trois jalons distincts. [Formats des versions](https://fr.minecraft.wiki/w/Formats_des_versions), [discussion archivistique sur Indev/Infdev](https://minecraft.wiki/w/Forum%3AChanging_Indev_and_Infdev_naming_scheme).
|
||||
|
||||
## 2009 : construire, puis survivre
|
||||
|
||||
| Date / période | Version ou étape | Contenu et portée |
|
||||
|---|---|---|
|
||||
| 10–16 mai 2009 | Pre-classic | Prototypes de déplacement, terrain cubique, placement et destruction. C’est une période de développement, non une sortie commerciale. [Chronologie Java](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis). |
|
||||
| 17 mai 2009 | Première diffusion publique de Classic | La construction précède la progression de survie complète. La réédition officielle de Classic illustre ce vocabulaire initial réduit de blocs. [Historique Java](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Hebrew_translation/Java_Edition), [rétrospective Mojang](https://www.minecraft.net/en-us/article/embrace-past-minecraft-classic). |
|
||||
| Fin mai–juin 2009 | Classic, tests multijoueurs puis extensions créatives | Le monde partagé apparaît très tôt. Le terrain, les liquides, la végétation et les matériaux décoratifs s’étoffent ; il ne faut pas leur attribuer rétrospectivement toutes leurs propriétés de survie. [Versions Classic](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis). |
|
||||
| 1er septembre 2009 | 0.24 SURVIVAL TEST | Santé, dégâts, affrontements et collecte structurent une branche de survie, parallèle au mode créatif. Les ennemis emblématiques appartiennent donc à une phase antérieure au craft complet. [Survival Test](https://de.minecraft.wiki/w/Survival_Test). |
|
||||
| 23 décembre 2009 | Début Indev | Éclairage dynamique et torches ; la survie devient progressivement un ensemble cohérent de systèmes. Les thèmes de monde expérimentaux n’équivalent pas encore à des dimensions accessibles par portail. [Historique des blocs](https://minecraft.wiki/w/Block_hardness), [période Indev](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Finnish_translation/Java_Edition). |
|
||||
|
||||
## 2010 : mondes finis, îles flottantes, machines et Nether
|
||||
|
||||
| Date / version | Apparition importante |
|
||||
|---|---|
|
||||
| **6 janvier, Indev 20100106** | Sélection des types « island », « floating », « flat », « original », des formes et des tailles. Le monde flottant est une possibilité historique du générateur. [Historique de génération](https://minecraft.wiki/w/Seed_%28world_generation%29). |
|
||||
| **7 janvier, Indev 20100107** | Les mondes flottants profonds peuvent avoir plusieurs couches d’îles ; thèmes normal et hell. [Historique de génération](https://minecraft.wiki/w/Seed_%28world_generation%29). |
|
||||
| **22 janvier, Indev 20100122** | Eau générée au-dessus du niveau marin et sur les îles flottantes ; ajustements des cavernes inondées. Ces expériences combinent déjà verticalité et hydrologie. [Historique de génération](https://minecraft.wiki/w/Seed_%28world_generation%29). |
|
||||
| 24–30 janvier, Indev | Coffres le 24 ; fabrication le 29 ; établi le 30. Les outils existaient auparavant, notamment distribués dans la maison Indev. Le stockage et la fabrication ne sont donc pas strictement simultanés. [Coffres](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Vietnamese_translation/R%C6%B0%C6%A1ng), [fabrication](https://ko.minecraft.wiki/w/%EC%A0%9C%EC%9E%91), [établi](https://de.minecraft.wiki/w/Numerische_Identifikation). |
|
||||
| 6 février, Indev | Blé, graines, houe, terre labourée et pain : première chaîne agricole. [Mojang, Wheat](https://www.minecraft.net/es-es/article/taking-inventory--wheat). |
|
||||
| 19 février, Indev | Le four remplace la cuisson/fonte par mise à feu des objets ; la transformation acquiert son bloc spécialisé. [Historique de la cuisson](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Indonesian_translation/Pembakaran). |
|
||||
| 27 février–mars, Infdev | Début du terrain extensible. Les anciens paramètres ne fonctionnent plus, puis disparaissent du menu le **27 mars**. « Infini » décrit une génération étendue à la demande, pas une dimension physiquement sans limites. [Types de monde](https://ja.minecraft.wiki/w/%E3%83%AF%E3%83%BC%E3%83%AB%E3%83%89%E3%82%BF%E3%82%A4%E3%83%97). |
|
||||
| Juin, Infdev | Panneaux, portes et échelles le 7 ; seaux le 15 ; interactions eau/lave productrices de pierre et d’obsidienne ; rails et wagonnets le **18 juin**. [Objets](https://minecraft.wiki/w/Items.png), [pierre renouvelable](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Toki_Pona_translation/Cobblestone). |
|
||||
| **25 juin**, Infdev | Donjons et générateurs de monstres : une structure rassemble danger répétable et butin. Les selles apparaissent à cette période. [Structures](https://it.minecraft.wiki/w/Struttura), [spawners](https://minecraft.wiki/w/Block_hardness), [objets](https://minecraft.wiki/w/Items.png). |
|
||||
| **2 juillet**, Alpha v1.0.1 | Minerai et poudre de redstone, torches, leviers, boutons, plaques et portes en fer. La redstone remplace les engrenages expérimentaux d’Indev. [Mojang, Redstone Ore](https://www.minecraft.net/fr-fr/article/redstone-ore), [engrenages](https://minecraft.wiki/w/Java_Edition_block_render_history/Gear). |
|
||||
| Juillet, Alpha v1.0.4–v1.0.14 | Mondes enneigés ; bateaux ; canne, papier, livres et bibliothèques ; argile, briques, slimes ; poules, œufs, jukebox et disques. Les wagonnets avec coffre et avec four donnent des usages logistiques au rail. [Objets Alpha](https://minecraft.wiki/w/Items.png), [dates des versions](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis). |
|
||||
| **3 août**, Alpha v1.0.15 ; août–septembre | Survie multijoueur ; puis barrières et spider jockeys. Boussole en v1.1.0, accroupissement et canne à pêche en v1.1.1 ; l’existence de la canne précède la pêche fonctionnelle de l’Halloween Update. [Chronologie Alpha](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis), [objets](https://minecraft.wiki/w/Items.png), [pêche](https://www.minecraft.net/fr-ca/article/desert). |
|
||||
| **30 octobre**, Alpha v1.2.0, Halloween Update | Nether et portails, ghasts et cochons zombies, nouveaux matériaux infernaux ; biomes formalisés dans l’Overworld, pêche et variations visuelles du ciel. Le Nether n’a pas encore ses forteresses. [Mojang, Nether Wastes](https://www.minecraft.net/fr-ca/article/around-block--nether-wastes), [Forest](https://www.minecraft.net/tr-tr/article/forest), [date](https://minecraft.wiki/w/Major_updates). |
|
||||
|
||||
Le précédent pertinent pour Sanctuary est précis : **Indev proposait réellement des îles flottantes, des couches verticales et de l’eau en altitude avant leur abandon dans Infdev**. Ce n’est ni une preuve que l’End existait déjà ni un événement vécu par des personnages. Le thème Indev « hell » changeait l’environnement d’un monde ; le Nether de 2010 introduit un autre espace et le passage par portail. Cette distinction empêche de fusionner trois expérimentations différentes. [Types de monde](https://ja.minecraft.wiki/w/%E3%83%AF%E3%83%BC%E3%83%AB%E3%83%89%E3%82%BF%E3%82%A4%E3%83%97), [portail en obsidienne](https://minecraft.wiki/w/obsidian).
|
||||
|
||||
## 2011 : Beta, infrastructures et aventure
|
||||
|
||||
| Sortie | Version | Contenu majeur |
|
||||
|---|---|---|
|
||||
| 20 décembre 2010 | Beta 1.0 | Nouvelle phase ; travail sur les inventaires et le multijoueur. Beta 1.1 suit surtout pour stabiliser. [Période Beta](https://fr.minecraft.wiki/w/%C3%89dition_Java_B%C3%AAta). |
|
||||
| 13 janvier 2011 | Beta 1.2 | Distributeur, bloc musical, charbon de bois, grès, lapis et teintures, gâteau, poulpes, bouleaux et épicéas. Les essences précèdent leurs planches distinctes de 2012. [Historique des blocs](https://minecraft.wiki/w/Block_hardness), [bois](https://minecraft.wiki/w/Oak-log-top), [Beta](https://fr.minecraft.wiki/w/%C3%89dition_Java_B%C3%AAta). |
|
||||
| 22 février | Beta 1.3 | Lits, répéteurs, nouvelles dalles ; saisie de graine et stockage des chunks par régions. La temporisation redstone et le saut de nuit deviennent des outils ordinaires. [Beta 1.3](https://uk.minecraft.wiki/w/Beta_1.3_%28Java_Edition%29). |
|
||||
| 31 mars | Beta 1.4 | Loups apprivoisables et cookies ; premier compagnon de survie de cette série. [Historique Beta](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis), [loups dans les taïgas](https://minecraft.wiki/w/Tiaga). |
|
||||
| 19 avril | Beta 1.5 | Météo, statistiques et achievements ; rails propulseurs et détecteurs. Les pousses spécifiques rendent bouleau et épicéa renouvelables. [Beta 1.5](https://es.minecraft.wiki/w/Java_Edition_Beta_1.5), [bois](https://minecraft.wiki/w/Oak-log-top). |
|
||||
| 26 mai | Beta 1.6 | Cartes, trappes et Nether multijoueur ; hautes herbes et végétation supplémentaire. [Beta 1.6](https://uk.minecraft.wiki/w/Beta_1.6_%28Java_Edition%29). |
|
||||
| 30 juin | Beta 1.7 | Pistons, pistons collants et cisailles : les circuits peuvent déplacer la matière. [Beta 1.7](https://zh.minecraft.wiki/w/Java%E7%89%88Beta_1.7). |
|
||||
| **14 septembre** | Beta 1.8, Adventure Update | Faim, sprint, expérience, créatif renouvelé ; Endermen, araignées venimeuses ; villages initialement vides, mines abandonnées, forteresses, ravins ; rivières et grands océans. Briques de pierre, vitres, barreaux, lianes et champignons géants enrichissent les ruines possibles. [Beta 1.8](https://zh.minecraft.wiki/w/Java%E7%89%88Beta_1.8?variant=zh-cn), [structures](https://it.minecraft.wiki/w/Struttura). |
|
||||
| **18 novembre** | **1.0.0** | Achèvement de l’Adventure Update : End, dragon et conclusion, enchantements, potions, élevage, hardcore, nouveaux habitants et forteresses du Nether. Ces systèmes ont plusieurs premières apparitions distinctes ci-dessous. [Date](https://minecraft.wiki/w/Major_updates), [Mojang, Ender Dragon](https://www.minecraft.net/pt-pt/article/ender-dragon). |
|
||||
|
||||
Les versions nommées **Beta 1.9 Prerelease** préparent la **1.0.0** ; il n’existe pas de sortie stable Beta 1.9 équivalente. La première introduit notamment villageois et forteresses du Nether ; ces villageois n’échangent pas encore. La **Prerelease 3 du 6 octobre** apporte table d’enchantement, alambic, œil de l’Ender et développement de l’élevage. La **Prerelease 4 du 13 octobre** rend l’End accessible et introduit le dragon encore en chantier. La Prerelease 6 de novembre complète notamment la conclusion. [Villageois historiques](https://minecraft.wiki/w/Villager_%28old%29), [Prerelease 3](https://es.minecraft.wiki/w/Java_Edition_Beta_1.9_Prerelease_3), [Prerelease 4](https://ru.minecraft.wiki/w/Beta_1.9_Prerelease_4_%28Java_Edition%29), [historique du protocole](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/wiki.vg_merge/Protocol_History).
|
||||
|
||||
## 2012 : habiter, échanger, écrire et administrer
|
||||
|
||||
| Sortie | Version | Contenu majeur et premier jalon utile |
|
||||
|---|---|---|
|
||||
| **12 janvier** | 1.1 | Langues supplémentaires, œufs d’apparition, monde superplat. Superflat arrive en **12w01a** ; il ne constitue pas le retour intégral des générateurs Indev. [1.1](https://ru.minecraft.wiki/w/1.1_%28Java_Edition%29), [types de monde](https://fr.minecraft.wiki/w/Type_de_monde). |
|
||||
| **1er mars** | 1.2.1 | Jungles, ocelots/chats, golems de fer, lampes redstone, sièges de villages ; format Anvil et hauteur **128 → 256**. Jungle dès **12w03a**, lampes **12w07a**. Les biomes sont conservés dans les données des chunks. [1.2.1](https://minecraft.wiki/w/Java_Edition_1.2.1). |
|
||||
| **22 mars** | 1.2.4 | Planches distinctes de bouleau, épicéa et jungle ; grès taillé et sculpté. Ces blocs décoratifs ne datent pas tous de l’arrivée des arbres correspondants. [Historique des blocs](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Swedish_translation/Block), [dates](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis). |
|
||||
| **1er août** | 1.3.1 | Commerce villageois et émeraudes, livres éditables, coffres de l’Ender, temples désert/jungle, crochets et fils, mode aventure, LAN et serveur intégré. Commerce et coffre arrivent en **12w21a** ; le stockage Ender devient personnel en **12w24a**. [1.3.1](https://es.minecraft.wiki/w/Java_Edition_1.3.1), [coffre de l’Ender](https://fr.minecraft.wiki/w/Coffre_de_l%27ender). |
|
||||
| **25 octobre** | 1.4.2, Pretty Scary Update | Wither, balise, sorcières, chauves-souris, squelettes wither, zombies villageois guérissables ; enclumes, cadres, pots, murets, carottes/pommes de terre ; commandes et règles de jeu. Bloc de commande et balise : **12w32a** ; enclume : **12w41a**. [Historique des blocs](https://minecraft.wiki/w/Block_hardness), [règles](https://fr.minecraft.wiki/w/R%C3%A8gle_de_jeu), [Mojang, Command Block](https://www.minecraft.net/ko-kr/article/block-week-command-block). |
|
||||
| **14 novembre** | 1.4.4 | Disque « wait » disponible ; « 11 » devient obtenable en survie ; commande `/enchant`. Une musique peut donc exister avant son objet accessible. [Annonce officielle](https://mcupdate.tumblr.com/post/35704186352/minecraft-144), [disques](https://minecraft.wiki/rest.php/v1/page/Music_Disc/html), [commande](https://pt.minecraft.wiki/w/Comandos/enchant). |
|
||||
| **20 décembre** | 1.4.6 | Livres enchantés et feux d’artifice ; premiers prototypes en **12w49a, le 7 décembre**. L’enchantement devient stockable dans un objet transmissible. [12w49a](https://zh.minecraft.wiki/w/12w49a?variant=zh-cn), [dates](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis). |
|
||||
|
||||
## 2013–2014 : logistique, diversité et monde programmable
|
||||
|
||||
| Sortie | Version | Contenu majeur et premiers jalons |
|
||||
|---|---|---|
|
||||
| **13 mars 2013** | 1.5, Redstone Update | Entonnoirs, comparateurs, droppers, capteurs solaires, coffres piégés, plaques pondérées, bloc de redstone, quartz et scoreboard. Comparateur/entonnoir apparaissent en **13w01a**, dropper en **13w03a**. Transport, mesure et transformation peuvent s’articuler en installations autonomes. [Redstone Update](https://es.minecraft.wiki/w/Redstone_Update), [dropper](https://fr.minecraft.wiki/w/Dropper), [Mojang, Hopper](https://www.minecraft.net/en-us/article/hopper). |
|
||||
| **1er juillet** | 1.6.1, Horse Update | Chevaux, ânes, mules, laisses, armures pour chevaux, étiquettes, foin, tapis, argile durcie/colorée et bloc de charbon ; packs de ressources et nouveau lanceur. Chevaux dès **13w16a, le 18 avril**. En 1.6.2, les bébés zombies apparaissent naturellement. [Horse Update](https://minecraft.wiki/w/Horse_Update), [premier nouveau lanceur](https://minecraft.wiki/w/Launcher_0.1). |
|
||||
| **25 octobre** | 1.7.2, The Update that Changed the World | Nouvelle distribution climatique : mesa, savane, forêt couverte, grandes taïgas, forêts fleuries, pics de glace ; nouveaux poissons pêchables, fleurs, podzol et verre coloré. Biomes **13w36a**, verre **13w41a**, bois d’acacia/chêne noir **13w43a** : leurs biomes précèdent leurs bois définitifs. Monde Amplifié. [Biomes](https://ko.minecraft.wiki/w/%EC%83%9D%EB%AC%BC_%EA%B5%B0%EA%B3%84), [blocs](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Esperanto_translation/Bloko), [types](https://fr.minecraft.wiki/w/Type_de_monde). |
|
||||
| **10 décembre** | 1.7.4 | Chicken jockeys, d’abord en **13w49a** : une petite version ajoute aussi du contenu, pas seulement des correctifs. [Dates](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis), [zombies villageois historiques](https://minecraft.wiki/w/Zombie_Villager_%28old%29), [Mojang](https://www.minecraft.net/en-us/article/meet-chicken-jockey). |
|
||||
| **2 septembre 2014** | 1.8, Bountiful Update | Monuments océaniques, gardiens, lapins, endermites ; granite/andésite/diorite, slime, prismarine, lanternes aquatiques, éponges humides, grès rouge ; bannières, porte-armures, portes/barrières par essence. Alex rejoint les apparences par défaut. [Date](https://minecraft.wiki/w/Major_updates), [Mojang, Ocean Monument](https://www.minecraft.net/en-us/article/ocean-monument), [blocs](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Indonesian_translation/Balok), [Alex](https://minecraft.wiki/w/Alex). |
|
||||
|
||||
Pour la 1.8, les étapes sont espacées : pierres et slime en **14w02a**, spectateur en **14w05a**, générateur personnalisé en **14w17a**, monuments en **14w25a**, lapins en **14w27a**. Ce sont des premières apparitions de développement ; septembre est leur diffusion stable commune. [Pierres](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Persian_translation/%D8%B3%D9%86%DA%AF), [spectateur](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Indonesian_translation/Mode_Penonton), [types](https://fr.minecraft.wiki/w/Type_de_monde), [structures](https://it.minecraft.wiki/w/Struttura), [créatures](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Indonesian_translation/Makhluk). L’enchantement change également en **14w02a** : dépense de lapis-lazuli et aperçu d’un enchantement possible, bien après l’introduction du minerai. [Historique de l’enchantement](https://minecraft.wiki/w/Minecraft_Wiki%3AProjects/Indonesian_translation/Penyihiran).
|
||||
|
||||
La programmation du monde suit une autre filiation : bloc de commande en 2012 ; `/summon` puis `/setblock` et `/tellraw` en 2013 ; `/clone` et `/fill` en **14w03a**, `/execute` en **14w07a**, `/worldborder` en **14w17a**, `/title` en **14w20a**. Ces outils sont ceux de l’administration et de la création de cartes, pas un ordinateur craftable vanilla. La 1.8.1 du 24 novembre ajoute notamment `doEntityDrops`, sans nouveau grand ensemble de structures. [Historique des commandes](https://zh.minecraft.wiki/w/%E5%91%BD%E4%BB%A4?variant=zh-tw), [Mojang, Command Block](https://www.minecraft.net/ko-kr/article/block-week-command-block), [règles de jeu](https://fr.minecraft.wiki/w/R%C3%A8gle_de_jeu).
|
||||
|
||||
## Limites et usage de cette chronologie
|
||||
|
||||
Les sources combinent Minecraft Wiki communautaire actuel, ses traductions et rétrospectives officielles Mojang ; une annonce officielle contemporaine a également été retrouvée pour la 1.4.4. Certaines pages ne sont consultables que par leurs extraits indexés. Les annonces originales Mojang de plusieurs versions anciennes et des builds perdus restent à recouper : ce dossier ne prétend pas constituer leur archivage exhaustif. Les mises à jour mineures de comportement ne sont pas toutes détaillées : par exemple la **1.3.2 du 16 août 2012** fait générer les grands chênes avec des branches horizontales. [Bois](https://minecraft.wiki/w/Oak-log-top), [dates](https://nl.minecraft.wiki/w/Javaeditie_versiegeschiedenis).
|
||||
|
||||
Pour une reconstruction jouable, vérifier le **build exact**, pas seulement sa famille : charbon de bois après les fours, planches distinctes après les arbres, commerce après les villageois, End après les Endermen. Les primitives actuelles ne doivent pas être rétroprojetées : ni échange villageois en Beta 1.8, ni îles externes de l’End de 1.9 dans la 1.0, ni programmation de survie par bloc de commande. Les rapprochements avec Galactium et les anciens joueurs devront rester explicitement des inventions de Sanctuary.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Minecraft Java, 2015–2026 : chronologie documentaire
|
||||
|
||||
Recherche arrêtée au **11 septembre 2026** : histoire publique du jeu, sans ajout au canon de Sanctuary. Périmètre : mises à jour de contenu depuis les snapshots de la 1.9, petits *drops* et premières apparitions expérimentales importantes. Les correctifs ordinaires ne sont pas détaillés. Les snapshots événementiels du **1er avril** ne sont pas inventoriés intégralement ; **20w14∞** fait l’objet d’un encadré ciblé.
|
||||
|
||||
Une **annonce** n’est pas encore jouable ; un **snapshot** est une version de développement ; une **préversion** ou une **release candidate** reste antérieure à la sortie stable. Un contenu proposé derrière une option expérimentale ne devient pas, de ce seul fait, du contenu normal de la version qui l’héberge. Cette distinction est explicitement utilisée par Mojang depuis les *feature flags*. [Explications et règles des fonctionnalités expérimentales, 1.19.3](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-19-3).
|
||||
|
||||
Sources : notes de Mojang sur Minecraft.net et historiques de **Minecraft Wiki**, dont sa [liste des grandes mises à jour](https://minecraft.wiki/w/Major_updates). Certaines pages du Wiki bloquaient la lecture automatisée : seuls leurs passages indexés ont alors été consultés, avec recoupement officiel disponible. Les dates ci-dessous désignent la disponibilité des versions ; les dates éditoriales d’articles traduits ou remaniés peuvent différer.
|
||||
|
||||
## 2015–2020 : destinations, métiers et production
|
||||
|
||||
| Version et disponibilité | Premières apparitions importantes | Contenu et distinction historique |
|
||||
| --- | --- | --- |
|
||||
| **Préparation de la 1.9 — 2015** | **15w31a, 29 juillet** : extension de l’End et shulkers. **15w41a, 7 octobre** : élytres. | Les cités de l’End ont été montrées avant leur disponibilité stable. Les élytres arrivent plus tard dans la même campagne de snapshots : il serait faux de leur donner automatiquement la date du premier snapshot. [Mojang : histoire des cités de l’End](https://www.minecraft.net/zh-hans/article/end-city), [Wiki : historique des créatures](https://minecraft.wiki/w/Minecraft_Wiki:Projects/Slovak_translation/Mob), [Wiki : historique des élytres parmi les outils](https://minecraft.wiki/w/Minecraft_Wiki:Projects/Indonesian_translation/Alat). |
|
||||
| **1.9 — Combat Update, 29 février 2016** | Les essais de 2015 précèdent cette sortie stable. | Refonte du combat, main secondaire, boucliers et flèches à effets ; l’End devient une destination d’exploration après le dragon, avec cités, vaisseaux, shulkers et élytres. Le shulker existe donc avant sa boîte transportable. [Mojang : shulker et Combat Update](https://www.minecraft.net/nl-nl/article/shulker), [Wiki : dates des mises à jour](https://minecraft.wiki/w/Major_updates). |
|
||||
| **1.10 — Frostburn Update, 8 juin 2016** | **16w20a, 18 mai** : ours polaires, husks, strays et magma. | Diversification du chaud et du froid, fossiles et blocs d’os, briques rouges du Nether et blocs de verrues. Le magma acquiert plus tard d’autres usages aquatiques ; ne pas attribuer ses colonnes de bulles à sa première apparition. [Mojang : histoire du magma](https://www.minecraft.net/zh-hant/article/block-week-magma), [Wiki : blocs de 16w20a](https://minecraft.wiki/w/Minecraft_Wiki:Projects/Finnish_translation/Kuutio), [Wiki : 1.10](https://minecraft.wiki/w/Major_updates). |
|
||||
| **1.11 — Exploration Update, 14 novembre 2016** | **16w39a, 28 septembre** : cartographes, nouveaux illageois, lamas, observateurs et boîtes de shulker. | Les manoirs donnent une destination aux cartes d’exploration vendues par les cartographes ; évocateurs, vindicateurs, vexes et totems accompagnent cette exploration. Les boîtes déplacent un stock complet d’objets, tandis que l’observateur enrichit la redstone. [Wiki : détail de 16w39a](https://de.minecraft.wiki/w/16w39a), [Wiki : chronologie des blocs](https://minecraft.wiki/w/Minecraft_Wiki:Projects/Finnish_translation/Kuutio), [dates](https://minecraft.wiki/w/Major_updates). |
|
||||
| **1.11.1 — 20 décembre 2016** | **16w50a, 15 décembre** prépare ce petit ajout de contenu. | Propulsion des élytres par fusées, pépites de fer, recyclage métallique et enchantement Sweeping Edge. Ne pas confondre le vol plané de la 1.9 avec cette autonomie de déplacement ultérieure. La note officielle a été actualisée pour le correctif 1.11.2 : son titre actuel couvre aussi celui-ci. [Note officielle 1.11.1/1.11.2](https://www.minecraft.net/en-us/article/minecraft-1112-released). |
|
||||
| **1.12 — World of Color, 7 juin 2017** | **17w06a, 8 février** : béton et terre cuite émaillée. **17w13a, 30 mars** : advancements, livre de recettes, perroquets. | Palettes colorées, lits teints et nouvelles possibilités décoratives. Les advancements remplacent les anciens achievements Java et peuvent récompenser ou guider des parcours personnalisés ; le livre de recettes accompagne la découverte de la fabrication. [Sortie officielle](https://www.minecraft.net/en-us/article/world-color-released), [17w13a](https://www.minecraft.net/en-us/article/minecraft-snapshot-17w13a), [Wiki : advancements](https://minecraft.wiki/w/Advancements). |
|
||||
| **1.13 — Update Aquatic, 18 juillet 2018** | **18w07a, 14 février** : nage, trident, tortues, algues, colonnes de bulles et phantoms. | Océans différenciés, coraux, poissons, dauphins, noyés, épaves, ruines et trésors enfouis composent une exploration maritime. Conduits et eau partageant certains blocs transforment aussi la construction. Cette version rassemble une évolution technique majeure et un ajout de monde. [Sortie officielle](https://www.minecraft.net/en-us/article/update-aquatic-out-java), [première vague aquatique](https://www.minecraft.net/en-us/article/minecraft-snapshot-18w07a). |
|
||||
| **1.13.1 — 22 août 2018** | Petit prolongement de la 1.13, pas un nouveau grand thème. | Ajout de coraux morts, changements écologiques des poissons et calamars, et commande `/forceload`. Celle-ci est un outil administratif de maintien des chunks, pas un bloc de survie analogue aux futures clés de Sanctuary. [Note officielle](https://www.minecraft.net/es-mx/article/minecraft-1131-released). |
|
||||
| **1.14 — Village & Pillage, 23 avril 2019** | **18w43a, 24 octobre 2018** : bambou, pandas, pillagers et première vague de nouveaux blocs/textures. | Villages à architecture régionale, postes de travail, nouvelles règles de métiers et d’échanges, cloches, raids, avant-postes et ravageurs. Tonneaux, fumoirs, hauts fourneaux, lanternes, échafaudages et tailleurs de pierre densifient les lieux de production. Ce n’est pas encore un système général de villageois ouvriers exécutant des commandes du joueur. [Snapshot](https://www.minecraft.net/de-de/article/minecraft-snapshot-18w43a), [sortie et métiers](https://www.minecraft.net/en-us/article/village---pillage-out-java-). |
|
||||
| **1.15 — Buzzy Bees, 10 décembre 2019** | **19w34a, 22 août** : abeilles, ruches et miel ; la production est déjà observable en snapshot. | Abeilles pollinisatrices, nids, ruches fabriquées, miel, rayons et blocs de miel créent une chaîne de récolte liée aux fleurs et au feu de camp. Les blocs collants enrichissent les machines ; les golems de fer deviennent réparables. [Premier snapshot officiel](https://feedback.minecraft.net/hc/en-us/articles/360032831011-Minecraft-Java-Edition-Snapshot-19W34A), [sortie](https://www.minecraft.net/en-us/article/buzzy-bees-out-now-in-java). |
|
||||
| **1.15.2 — 21 janvier 2020** | Petit prolongement de l’apiculture. | Un chêne ou bouleau poussé depuis une jeune pousse près d’une fleur, à deux blocs au plus et au même niveau, a **5 %** de chances de porter un nid. Les nids deviennent ainsi renouvelables par plantation. Ajout des règles `doPatrolSpawning` et `doTraderSpawning`. [Note officielle](https://feedback.minecraft.net/hc/en-us/articles/360038800232-Minecraft-Java-Edition-1-15-2). |
|
||||
| **1.16 — Nether Update, 23 juin 2020** | **20w06a, 5 février** : nouvelles forêts, vallée des âmes, hoglins et netherite. | Basalte, blackstone, végétations fongiques, piglins et troc, arpenteurs, ancres de réapparition, bastions et portails en ruine rendent le Nether plus habitable et plus historique. Le minerai de débris antiques fournit une nouvelle étape matérielle après le diamant. [Premier snapshot](https://feedback.minecraft.net/hc/en-us/articles/360039129472-Minecraft-Java-Edition-Snapshot-20W06A), [sortie officielle](https://www.minecraft.net/en-us/article/nether-update-java). |
|
||||
| **1.16.2 — 11 août 2020** | **20w27a, 1er juillet** : piglin brute. | Les brutes gardent les bastions et ne se laissent pas détourner par l’or : une spécialisation de défense s’ajoute à une structure existante. Ce petit ajout mérite une place séparée des correctifs ordinaires. [Snapshot](https://www.minecraft.net/de-de/article/minecraft-snapshot-20w27a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-16-2). |
|
||||
|
||||
> **20w14∞ (`20w14infinite`), 1er avril 2020 — expérience événementielle.** Mojang propose d’écrire quelques mots dans un livre puis de le jeter dans un portail du Nether : le texte donne accès à un monde particulier. L’annonce humoristique présente plus de deux milliards de mondes et une machine de Turing tridimensionnelle. [Présentation originale de Mojang](https://www.minecraft.net/en-us/article/every-update-imaginable-coming-minecraft).
|
||||
>
|
||||
> Ce rapprochement entre écriture, calcul et destinations peut nourrir la réflexion sur Galactium et les Indoors. Il ne décrit ni un contenu Java stable, ni une compatibilité technique, ni un système déjà implémenté dans Sanctuary.
|
||||
|
||||
## 2021–2024 : profondeur, traces et systèmes composables
|
||||
|
||||
| Version et disponibilité | Premières apparitions importantes | Contenu et distinction historique |
|
||||
| --- | --- | --- |
|
||||
| **1.17 — Caves & Cliffs, partie I, 8 juin 2021** | **20w45a, 4 novembre 2020** : cuivre, améthyste, longue-vue, paratonnerre, verre teinté. **21w08a, 24 février 2021** : le grimstone devient deepslate. | Géodes, cuivre oxydable, pierre des abîmes, mousses, spéléothèmes, axolotls, chèvres et poulpes luisants. Les matériaux arrivent avant la grande transformation stable du relief : la 1.17 normale ne possède pas encore l’Overworld complet de la 1.18. [20w45a](https://feedback.minecraft.net/hc/en-us/articles/360051653692-Minecraft-Java-Edition-Snapshot-20W45A), [21w08a](https://www.minecraft.net/pl-pl/article/minecraft-snapshot-21w08a), [Wiki : partie I](https://minecraft.wiki/w/Java_Edition_guides/Caves_%26_Cliffs%3A_Part_I). |
|
||||
| **1.18 — Caves & Cliffs, partie II, 30 novembre 2021** | **21w06a, 10 février** : premières cavernes à bruit et aquifères ; retirés de la série 1.17 le **14 avril**, puis retravaillés séparément. | Montagnes et cavernes profondes, lush caves et dripstone caves naturellement distribuées ; Overworld de **−64 à 319**, soit **384 couches**. Nouvelles distributions des minerais, grandes veines cuivre/granit et fer/tuf. [Premier essai](https://www.minecraft.net/es-mx/article/minecraft-snapshot-21w06a), [séparation des versions](https://www.minecraft.net/en-us/article/minecraft-snapshot-21w15a), [sortie](https://feedback.minecraft.net/hc/en-us/articles/4415128577293-Minecraft-Java-Edition-1-18). |
|
||||
| **1.19 — The Wild Update, 7 juin 2022** | **17 février** : essai Deep Dark distinct ; **22w13a, 31 mars** : allay et cités anciennes dans la série régulière. | Deep Dark, sculk, Warden et cités anciennes ; mangrove, boue, briques de boue, grenouilles, têtards, allays et bateaux-coffres. Les cités offrent déjà une ruine souterraine documentée ; elles ne prouvent pas à elles seules l’existence d’un ancien peuple précis. [Essai Deep Dark](https://www.minecraft.net/en-us/article/a-very-scary-snapshot), [22w13a](https://www.minecraft.net/en-us/article/minecraft-snapshot-22w13a), [sortie](https://www.minecraft.net/en-us/article/the-wild-update-out-today-java). |
|
||||
| **1.19.1 — 27 juillet 2022** | **22w24a, 15 juin** : duplication des allays. | Un allay dansant près d’un jukebox peut se dupliquer avec un éclat d’améthyste. La version introduit aussi le signalement des conversations ; cette histoire du logiciel reste distincte d’une explication fictionnelle du monde. [Snapshot](https://feedback.minecraft.net/hc/en-us/articles/6968038003085-Minecraft-Java-Edition-Snapshot-22w24a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-19-1). |
|
||||
| **1.19.3 — 7 décembre 2022** | **22w42a, 19 octobre** : premiers essais des contenus de la future 1.20 derrière une option. | Inventaire créatif réorganisé, nouveaux skins par défaut et vex remanié. Chameaux, bois de bambou et autres contenus activables pour tester la 1.20 ne doivent pas être datés comme une disponibilité normale en 1.19.3. [Note officielle et séparation des expérimentations](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-19-3). |
|
||||
| **1.19.4 — 14 mars 2023** | **23w06a, 8 février** : entités d’affichage ; **23w07a, 15 février** : entités d’interaction. | Affichages de blocs, objets et textes, commandes `/ride` et `/damage`, élevage équin amélioré, jukebox automatisable : de nouveaux outils de mise en scène. [Sortie](https://feedback.minecraft.net/hc/en-us/articles/13987663727757-Minecraft-Java-Edition-1-19-4), [23w06a](https://www.minecraft.net/zh-hant/article/minecraft-snapshot-23w06a), [23w07a](https://www.minecraft.net/de-de/article/minecraft-snapshot-23w07a). |
|
||||
| **1.20 — Trails & Tales, 7 juin 2023** | **23w07a, 15 février** : archéologie, sniffer et cerisiers, alors expérimentaux. | Sables/graviers suspects, brossage, tessons, pots décorés, ruines des sentiers, graines anciennes et sniffer ; cerisiers, bambou constructible, chameaux, bibliothèques sculptées, enseignes suspendues et ornements d’armure. Des objets portent désormais des traces collectables du passé, sans récit historique unique imposé. [Snapshot](https://www.minecraft.net/de-de/article/minecraft-snapshot-23w07a), [sortie officielle](https://www.minecraft.net/en-us/article/trails-tales-update-here), [Wiki : date 1.20](https://minecraft.wiki/w/Java_Edition_1.20). |
|
||||
| **1.20.2 — 21 septembre 2023** | Expérimentation séparée du rééquilibrage des échanges. | Plus de diamants dans les profondeurs, macros de fonctions, commande aléatoire et nouvelles possibilités de packs. La réforme expérimentale des bibliothécaires/cartographes n’est pas automatiquement la règle normale des villages. [Note officielle](https://feedback.minecraft.net/hc/en-us/articles/19703470383757-Minecraft-Java-Edition-1-20-2). |
|
||||
| **1.20.3 — 5 décembre 2023** | **23w41a, 11 octobre** prépare les pots fonctionnels. | Les pots décorés stockent une pile d’objets et interagissent avec hoppers, droppers et comparateurs ; les chauves-souris changent d’apparence. Ajout de `/tick`. Les chambres d’épreuves de l’expérimentation 1.21 ne sont pas encore la génération normale. [Snapshot](https://www.minecraft.net/de-de/article/minecraft-snapshot-23w41a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-20-3). |
|
||||
| **1.20.5 — Armored Paws, 23 avril 2024** | **23w51a, 18 décembre 2023** : tatou et armure de loup ; **24w10a, 6 mars 2024** : variantes de loups. | Tatous, écailles brossables et armures réparables/teignables pour les loups ; huit variantes complètent le loup connu, selon les biomes. Cette protection animale précède Tricky Trials : elle n’est pas un ajout de la 1.21. [23w51a](https://www.minecraft.net/sv-se/article/minecraft-snapshot-23w51a), [24w10a](https://www.minecraft.net/pl-pl/article/minecraft-snapshot-24w10a), [sortie](https://www.minecraft.net/ru-ru/article/minecraft-java-edition-1-20-5). |
|
||||
| **1.21 — Tricky Trials, 13 juin 2024** | **23w42a, 18 octobre 2023** : crafter ; **23w45a, 8 novembre** : chambres d’épreuves et Breeze ; **24w11a, 14 mars 2024** : masse. | Chambres modulaires, générateurs d’épreuves, coffres-forts à clés, épreuves inquiétantes, Breeze et Bogged ; masse, charges de vent, tuf travaillé et nouveaux cuivres. Le crafter ouvre la fabrication automatisée vanilla. [Crafter](https://www.minecraft.net/pt-pt/article/minecraft-snapshot-23w42a), [chambres](https://www.minecraft.net/nl-nl/article/minecraft-snapshot-23w45a), [masse](https://www.minecraft.net/es-es/article/minecraft-snapshot-24w11a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21). |
|
||||
| **1.21.2 — Bundles of Bravery, 22 octobre 2024** | Les bundles existaient à l’essai dès **20w45a**, en 2020. | Les bundles deviennent enfin du contenu normal, avec variantes teintes. Les bébés dauphins/calamars et le Hardcore pour Realms Java complètent le drop. Ce n’est pas la première apparition du mode Hardcore dans Java, ni une sortie stable des bundles en 1.17. [Premier essai](https://feedback.minecraft.net/hc/en-us/articles/360051653692-Minecraft-Java-Edition-Snapshot-20W45A), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-2). |
|
||||
| **1.21.4 — The Garden Awakens, 3 décembre 2024** | **24w40a, 2 octobre** : Pale Garden expérimental. | Jardin pâle, chêne pâle, mousses suspendues, Creaking et cœur associé, résine et eyeblossoms. L’écosystème nocturne lie comportement de créature et bloc caché dans un arbre. Les premières annonces et le premier essai ne représentent pas encore tous les blocs du drop final. [Snapshot](https://www.minecraft.net/en-us/article/minecraft-snapshot-24w40a), [sortie](https://www.minecraft.net/ko-kr/article/minecraft-java-edition-1-21-4). |
|
||||
|
||||
## 2025–11 septembre 2026 : drops et frontière contemporaine
|
||||
|
||||
| Version et disponibilité | Premières apparitions importantes | Contenu et distinction historique |
|
||||
| --- | --- | --- |
|
||||
| **1.21.5 — Spring to Life, 25 mars 2025** | **25w02a, 8 janvier** : cochons climatiques, fleurs sauvages, feuilles mortes et particules de feuilles. | Variantes chaudes/froides des cochons, vaches et poules, nouveaux sons de loups, arbres tombés, buissons à lucioles, herbes sèches et fleurs de cactus. L’identité climatique passe aussi par les petites ambiances et la faune. [Snapshot](https://www.minecraft.net/ja-jp/article/minecraft-snapshot-25w02a), [sortie](https://www.minecraft.net/sv-se/article/minecraft-java-edition-1-21-5), [Wiki](https://minecraft.wiki/w/Java_Edition_1.21.5). |
|
||||
| **1.21.6 — Chase the Skies, 17 juin 2025** | **25w15a, 8 avril** : ghast desséché, ghastling, happy ghast, harnais et barre de localisation. | Monture volante coopérative, élevage du ghast, selles fabriquables, nouvelles interactions de laisses et musique. Java reçoit des améliorations du brouillard et des nuages : la note parle d’étapes vers Vibrant Visuals, pas de sa livraison complète sur Java. [Snapshot](https://www.minecraft.net/pl-pl/article/minecraft-snapshot-25w15a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-6). |
|
||||
| **1.21.7 — 30 juin 2025** | Petit ajout entre deux grands drops. | Disque **Lava Chicken**, obtenu sur un chicken jockey, et tableau **Dennis**, en plus des correctifs. Il faut conserver cette ligne si l’on suit l’apparition d’objets culturels et décoratifs, même si le numéro ressemble à un simple correctif. [Note officielle](https://www.minecraft.net/es-es/article/minecraft-java-edition-1-21-7). |
|
||||
| **1.21.9 — The Copper Age, 30 septembre 2025** | **25w31a, 29 juillet** : golem/coffre de cuivre, équipement et étagères. | Le cuivre devient outil, armure, stockage, décor et compagnon de tri ; les golems s’oxydent en statues. Les étagères exposent des objets et peuvent échanger des groupes de cases avec la barre rapide. Le minerai, lui, existait depuis 1.17. [Snapshot](https://www.minecraft.net/en-us/article/minecraft-snapshot-25w31a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-9). |
|
||||
| **1.21.11 — Mounts of Mayhem, 9 décembre 2025** | **25w41a, 9 octobre** : nautiles, nautiles zombies, lances et chevaux zombies naturellement accessibles. | Combat monté, lance, montures aquatiques et armures associées ; camel husk, parched et armure de cheval en netherite complètent la sortie. Le cheval zombie existait auparavant comme entité : sa disponibilité naturelle est une évolution distincte. [Snapshot](https://www.minecraft.net/en-us/article/minecraft-snapshot-25w41a), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-11). |
|
||||
| **26.1 — Tiny Takeover, 24 mars 2026** | Série inaugurée le **16 décembre 2025** ; **snapshot 2, 7 janvier 2026** : première vague de bébés remaniés et étiquettes fabriquables. | Nouveaux modèles/sons de bébés animaux, étiquettes fabriquables et pissenlit doré permettant de suspendre leur croissance. Cette version suit Mounts of Mayhem ; la nouvelle numérotation ne signifie pas un nouveau jeu. [Snapshot 2](https://www.minecraft.net/tr-tr/article/minecraft-26-1-snapshot-2), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-1), [numérotation officielle](https://www.minecraft.net/pl-pl/article/minecraft-new-version-numbering-system). |
|
||||
| **26.2 — Chaos Cubed, 16 juin 2026** | **Snapshot 1, 7 avril** : grottes de soufre, sulfur cube, cinabre et nouvelles sources. | Géologie sulfurée, soufre/cinabre constructibles, soufre puissant et interactions eau/chaleur produisant des geysers. Le sulfur cube absorbe des blocs et change de comportement physique. Vulkan est proposé expérimentalement : ne pas le transformer en disponibilité graphique universelle. [Premier snapshot](https://www.minecraft.net/da-dk/article/minecraft-26-2-snapshot-1), [sortie](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-2). |
|
||||
| **26.3 — développement, pas encore stable au 11 septembre 2026** | **Snapshot 1, 23 juin** ; **préversion 1, 1er septembre** ; **RC1, 10 septembre**. | Dappled Forest, peupliers et feuilles colorées, champignons de tronc, arbustes rouges, camps abandonnés, dalles/escaliers de laine apparaissent en développement. Ne pas inventer une date de sortie stable ou confondre ce numéro avec Bedrock. [Nouveautés initiales](https://www.minecraft.net/en-us/article/minecraft-26-3-snapshot-1), [préversion](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-1), [manifeste primaire Mojang](https://piston-meta.mojang.com/mc/game/version_manifest_v2.json). |
|
||||
|
||||
## Bornes d’utilisation pour Sanctuary
|
||||
|
||||
Au relevé du **11 septembre 2026**, le manifeste Mojang donne `latest.release = 26.2` et `latest.snapshot = 26.3-rc-1`. Cette dernière est datée du **10 septembre à 11:28:25 UTC** ; `26.3-pre-2`, utilisée comme base du dépôt, est datée du **4 septembre**. Le manifeste classe également les préversions et RC dans la catégorie technique `snapshot` : cela ne les rend pas stables. [Manifeste des versions Java](https://piston-meta.mojang.com/mc/game/version_manifest_v2.json).
|
||||
|
||||
Le **16 mars 2026** est le repère de fondation fourni de mémoire par Koka, **en cours d’audit**. Si cette date est confirmée, **1.21.11** est alors la dernière Java stable ; **26.1 sort huit jours après**, le 24 mars. Les sorties ultérieures constituent l’histoire publique contemporaine de la partie ; leur transformation en événements vécus par les joueurs appartient à la conception, pas à cette recherche.
|
||||
|
||||
Enfin, une cité de l’End, un bastion, une chambre d’épreuves ou un camp abandonné est un fait de génération documenté. En déduire ses constructeurs, leur disparition ou une catastrophe serait une interprétation supplémentaire. La chronologie permet de dater les matériaux, outils et formes de lieux ; elle ne fournit pas, à elle seule, un canon expliquant les ruines.
|
||||
@@ -1,175 +0,0 @@
|
||||
# Personnages de Minecraft : chronologie et limites documentaires
|
||||
|
||||
Recherche du **11 septembre 2026**, destinée à la conception de Sanctuary.
|
||||
Ce dossier sépare les dates de développement, les apparitions publiques et
|
||||
l’invention narrative. **Efe** est l’orthographe officielle, confirmée pour
|
||||
Sanctuary par Koka ; l’ancienne transcription « Mefe » n’est pas un dixième
|
||||
personnage. Aucun rôle individuel ni ordre d’arrivée fictif n’est fixé ici.
|
||||
|
||||
## Ce que l’on peut dater
|
||||
|
||||
La chronologie exploitable est **Steve, puis Alex, puis un groupe de sept**.
|
||||
Une date d’ajout de skin décrit un choix d’apparence disponible pour le joueur ;
|
||||
elle ne date ni la naissance du personnage ni son arrivée dans un monde fictif.
|
||||
De même, le premier modèle visible, l’annonce d’une version et le contenu du
|
||||
Launcher ne sont pas nécessairement contemporains.
|
||||
|
||||
Les sources Mojang sont prioritaires pour les annonces et les contenus livrés.
|
||||
Minecraft Wiki sert à reconstituer les premières versions, avec prudence :
|
||||
plusieurs pages anciennes ne sont accessibles ici que par leurs extraits
|
||||
indexés. Les liens vers d’anciens billets Mojang ou tweets identifiés mais non
|
||||
relus sont signalés comme tels ; ils ne sont pas présentés comme des preuves
|
||||
primaires intégralement vérifiées.
|
||||
|
||||
## Steve : distinguer modèle, joueur et nom
|
||||
|
||||
Le modèle humain associé rétrospectivement à Steve apparaît dans
|
||||
**rd-132328, le 13 mai 2009**. Il sert d’abord à des entités autonomes. Son modèle
|
||||
est ensuite repris pour représenter les autres joueurs dans le premier test
|
||||
multijoueur **0.0.15a, le 31 mai 2009**. Il faut donc distinguer ces deux étapes,
|
||||
et éviter d’écrire que Steve était déjà un personnage nommé avec une biographie
|
||||
dans le tout premier prototype. La page actuelle du Wiki décrit cette filiation
|
||||
entre mob et joueur ; l’ancienne page du test multijoueur précise sa date.
|
||||
[Minecraft Wiki : Mob (entity)](https://minecraft.wiki/w/Mob_%28entity%29),
|
||||
[historique 0.0.15a conservé sur l’ancien Wiki](https://minecraft.fandom.com/wiki/Java_Edition_Classic_0.0.15a_%28Multiplayer_Test_1%29).
|
||||
|
||||
Le **17 mai 2009** reste la date anniversaire publique célébrée par Mojang.
|
||||
Elle ne doit pas effacer les essais des jours précédents. Pour un calendrier
|
||||
Sanctuary comptant les jours depuis « le début de Minecraft », c’est un repère
|
||||
public solide, tandis que le 13 mai peut désigner la préhistoire des prototypes.
|
||||
[Mojang, anniversaire du 17 mai 2024](https://www.minecraft.net/en-us/article/the-15th-anniversary-cape).
|
||||
|
||||
Le nom **Steve** est documenté bien plus tard, vers **octobre 2010**. Le Wiki
|
||||
renvoie à une intervention de Notch et à l’histoire du personnage invité dans
|
||||
*Super Meat Boy*. Le tweet original n’a pas pu être relu pendant cette recherche :
|
||||
retenir le mois, sans lui attribuer un jour ni reproduire une citation incertaine.
|
||||
L’orthographe interrogative « Steve? » appartient à cette histoire du nom ; elle
|
||||
ne prouve pas une identité secrète. Le réexamen des premiers noms communautaires
|
||||
montre justement pourquoi un surnom ancien ne suffit pas à établir un canon.
|
||||
[Discussion documentaire du Wiki et référence au tweet](https://minecraft.wiki/w/Talk%3AMob_%28entity%29).
|
||||
|
||||
## Alex : 2014, avec une nuance sur les dates
|
||||
|
||||
Alex est introduite dans **Java 1.8-pre1**, annoncée publiquement le
|
||||
**22 août 2014**, puis intégrée à la version stable **1.8 du 2 septembre 2014**.
|
||||
Cette apparition accompagne le modèle aux bras plus fins. Ce n’est pas une
|
||||
nouvelle espèce ni une profession imposée au joueur.
|
||||
[Minecraft Wiki : Alex](https://minecraft.wiki/w/Alex),
|
||||
[historique de la Bountiful Update](https://minecraft.fandom.com/wiki/Bountiful_Update).
|
||||
|
||||
Le manifeste officiel contient toutefois un `releaseTime` au **21 août** pour
|
||||
1.8-pre1. Cette différence est conservée : l’horodatage du paquet ne remplace pas
|
||||
la date de publication attestée dans l’historique de l’annonce. Pour la version
|
||||
stable, les métadonnées confirment le 2 septembre. L’ancien billet Mojang de la
|
||||
préversion a été retrouvé par référence mais son contenu original n’était pas
|
||||
accessible à la relecture.
|
||||
[Métadonnées Mojang de 1.8-pre1](https://piston-meta.mojang.com/v1/packages/00ddc59925abc10e08047c94657e3365b1e031d6/1.8-pre1.json),
|
||||
[métadonnées de 1.8](https://piston-meta.mojang.com/v1/packages/9eb165eef46294062d8698c8a78e8ac914949e7a/1.8.json).
|
||||
|
||||
## Ari, Sunny, Kai, Zuri, Efe, Makena et Noor : une même promotion
|
||||
|
||||
Les sept nouveaux skins sont annoncés ensemble à **Minecraft Live, le
|
||||
15 octobre 2022**. Mojang précise que Sunny, Efe et Noor étaient déjà visibles
|
||||
dans les bandes-annonces de *The Wild Update*. Une apparition promotionnelle
|
||||
antérieure ne signifie donc pas que leur skin était déjà disponible dans le jeu.
|
||||
[Mojang, récapitulatif de Minecraft Live 2022](https://www.minecraft.net/en-us/article/minecraft-live-2022-the-recap).
|
||||
|
||||
Le déploiement comporte plusieurs jalons :
|
||||
|
||||
| Date | Événement établi | Portée exacte |
|
||||
| --- | --- | --- |
|
||||
| 20 octobre 2022 | Article présentant les sept nouveaux skins | Présentation officielle de leurs noms et du choix élargi. |
|
||||
| 20 octobre 2022 | Launcher 2.3.462 | Le Wiki recense leur ajout parmi les skins Java proposés par le Launcher. |
|
||||
| 9 novembre 2022 | Snapshot Java 22w45a | Ajout des nouveaux skins par défaut pour les joueurs hors ligne. |
|
||||
| 29 novembre 2022 | Bedrock 1.19.50 | Nouveaux skins sélectionnables dans le Dressing Room. |
|
||||
| 29 novembre 2022 | Vidéo officielle de lancement | Mojang annonce leur disponibilité au lancement du jeu à partir de cette date. |
|
||||
| 7 décembre 2022 | Java 1.19.3 stable | La version stable reprend les nouveaux skins hors ligne. |
|
||||
|
||||
Sources de chaque jalon :
|
||||
[présentation Mojang du 20 octobre](https://www.minecraft.net/en-us/article/introducing-new-default-skins),
|
||||
[Wiki : Launcher 2.3.462](https://minecraft.wiki/w/Launcher_2.3.462),
|
||||
[snapshot 22w45a](https://www.minecraft.net/en-us/article/minecraft-snapshot-22w45a),
|
||||
[Bedrock 1.19.50](https://www.minecraft.net/en-us/article/1-19-50-update-available-bedrock),
|
||||
[vidéo Minecraft du 29 novembre](https://www.youtube.com/watch?v=oXKVfLTrdBM),
|
||||
[Java 1.19.3](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-19-3).
|
||||
|
||||
Ces sources ne donnent **aucune succession individuelle d’ajouts** entre les
|
||||
sept. L’ordre « Ari, Sunny, Kai, Zuri, Efe, Makena, Noor » peut être conservé pour
|
||||
présenter le groupe dans Sanctuary, mais pas comme une chronologie Minecraft.
|
||||
Une éventuelle succession de leurs arrivées sur Sanctuary devra être écrite et
|
||||
validée comme fiction. Il ne faut pas inventer sept mises à jour différentes
|
||||
pour obtenir artificiellement neuf époques correspondant aux neuf noms.
|
||||
|
||||
## Apparences officielles et biographies inventées
|
||||
|
||||
Mojang présente les skins comme des apparences permettant aux joueurs de raconter
|
||||
leurs propres histoires. Les neuf constituent des options, avec des modèles
|
||||
larges et fins ; l’article ne leur attribue pas neuf spécialités nécessaires au
|
||||
fonctionnement du monde. Une tenue, une bande-annonce ou un accessoire ne suffit
|
||||
pas à imposer pour toujours le métier, la personnalité ou la filiation d’un
|
||||
personnage.
|
||||
[Mojang : Introducing new default Minecraft skins](https://www.minecraft.net/en-us/article/introducing-new-default-skins).
|
||||
|
||||
Pour Sanctuary, « Steve a construit cet atelier » ou « Efe a travaillé sur cette
|
||||
installation » sera donc une décision d’auteur. Elle pourra s’appuyer sur le
|
||||
catalogue d’objets disponible à une époque, mais elle ne devra pas être présentée
|
||||
comme une révélation de Mojang. Les adaptations, vidéos promotionnelles et jeux
|
||||
dérivés demandent eux aussi un périmètre explicite : on ne fusionne pas leurs
|
||||
scènes en une biographie unique par défaut.
|
||||
|
||||
## Endermen, anciens bâtisseurs et Herobrine
|
||||
|
||||
L’explication officielle de la **création des Endermen**, donnée par Jens dans
|
||||
un article de 2017, cite l’inspiration du Slenderman et indique que l’End a été
|
||||
conçu après le mob. C’est une histoire de développement. Elle n’établit pas
|
||||
qu’une civilisation humaine s’est transformée en Endermen. Le récit des
|
||||
« anciens bâtisseurs » et de leur métamorphose doit rester identifié comme
|
||||
théorie communautaire ou réécriture propre à Sanctuary.
|
||||
[Mojang : Meet the Enderman](https://www.minecraft.net/en-us/article/meet-enderman).
|
||||
|
||||
La présence de villes et de navires dans l’End fournit des vestiges à interpréter.
|
||||
La présentation officielle du biome décrit leurs habitants, leurs dangers et
|
||||
leur réutilisation possible comme camp ; elle ne nomme pas Steve, Alex ou les
|
||||
sept comme constructeurs. L’absence de signature peut nourrir le mystère sans
|
||||
obliger à trancher immédiatement l’origine de chaque structure.
|
||||
[Mojang : End Highlands](https://www.minecraft.net/en-us/article/around-block--end-highlands).
|
||||
|
||||
La mention **« Removed Herobrine »** figure réellement dans des changelogs,
|
||||
par exemple celui de Java 1.16.2. Elle ne constitue pas une preuve de présence
|
||||
antérieure d’un mob Herobrine : le texte officiel archivé des suggestions
|
||||
écartées refuse les creepypastas. Une référence au folklore peut être étudiée
|
||||
séparément ; elle ne transforme pas Herobrine en dixième skin historique ni en
|
||||
cause établie de la disparition des neuf.
|
||||
[Java 1.16.2](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-16-2),
|
||||
[politique archivée, mise à jour le 7 mai 2020](https://feedback.minecraft.net/hc/en-us/articles/360005029872--Archived-Previously-Considered-Suggestions).
|
||||
|
||||
## Les anciens modèles de Dock ne complètent pas le groupe
|
||||
|
||||
Rana et les variantes autrefois appelées « Steve », « Black Steve » et
|
||||
« Beast Boy » appartiennent aux essais de modèles d’Indev, fin 2009 et janvier
|
||||
2010. Le Wiki actuel distingue le nom de Rana des trois surnoms communautaires
|
||||
des autres variantes. Ils ne constituent pas trois prédécesseurs biographiques
|
||||
du skin Steve et ne doivent pas être ajoutés par défaut aux neuf personnages
|
||||
retenus. Leur intérêt éventuel pour Sanctuary serait celui d’une trace des
|
||||
formes abandonnées du développement.
|
||||
[Minecraft Wiki : modèles historiques du mob](https://minecraft.wiki/w/Mob_%28entity%29).
|
||||
|
||||
## Conséquences pour la mythologie de Sanctuary
|
||||
|
||||
La matière historique permet d’imaginer des couches d’occupation correspondant à
|
||||
des possibilités successives du jeu : construire, transporter, automatiser,
|
||||
observer, explorer plus loin. Il est préférable de dater une **solution** par ses
|
||||
matériaux et ses techniques, puis de choisir ses auteurs, plutôt que d’attribuer
|
||||
automatiquement chaque grande mise à jour à un nouveau personnage.
|
||||
|
||||
Les neuf peuvent avoir modifié les mêmes installations, laissé des réparations,
|
||||
poursuivi le travail d’autres personnes ou retrouvé leurs méthodes. Cette piste
|
||||
sert la formule retenue : une infrastructure dont l’auteur disparaît derrière
|
||||
son fonctionnement. Elle demeure une proposition de récit, sans généalogie ni
|
||||
responsabilité encore assignée.
|
||||
|
||||
Galactium, le cube originel, ses six faces et son septième champ central sont
|
||||
des éléments propres à la conception de Sanctuary. La chronologie documentée
|
||||
n’impose pas de faire correspondre les sept skins de 2022 aux sept champs, ni
|
||||
de placer Steve et Alex hors de la cosmologie. Ce rapprochement peut être
|
||||
discuté pour sa valeur narrative, mais ne découle pas de l’histoire officielle.
|
||||
@@ -1,127 +0,0 @@
|
||||
# Redstone Computer — référence et proposition pour Sanctuary
|
||||
|
||||
**Lecture du 10 septembre 2026, WG-26.** Référence locale demandée par l’auteur :
|
||||
`/Users/koka/Documents/sanctuary/Redstone/redstone_computer`.
|
||||
Le README et les sources ont été lus, sans compilation ni essai en jeu. Le
|
||||
manifeste indique Redstone Computer 2.0.0, Minecraft 26.1.2, auteur KOKA99CAB et
|
||||
licence GPL-3.0-or-later. Cette lecture ne vérifie pas un portage vers 26.3.
|
||||
|
||||
Le dossier historique `26.2/redstoner` a été consulté ponctuellement pour sa VM.
|
||||
Ces deux références restent intactes. Aucune de leurs classes n’est importée
|
||||
dans Sanctuary par ce cahier.
|
||||
|
||||
## Capacités retrouvées dans le code
|
||||
|
||||
Les trois profils sont définis dans `program/ProgramTarget.java` :
|
||||
|
||||
| Machine historique | RAM en octets logiques | Budget d’instructions par tick |
|
||||
| --- | --- | --- |
|
||||
| Redstone Computer 8 bits | 256 | 96 |
|
||||
| Redstone Controller | 1 024 | 256 |
|
||||
| Personal Computer | 4 096 | 1 024 |
|
||||
|
||||
Ces valeurs décrivent l’ancienne version ; elles ne sont pas des réglages validés
|
||||
pour les futurs contrôleurs Sanctuary.
|
||||
|
||||
- `vm/RedstoneProgram.java` : assembleur interprété, labels, sauts, calculs,
|
||||
logique, registres A–D sur 8 bits, indicateurs de comparaison et RAM adressable.
|
||||
- `block/entity/RedstoneComputerBlockEntity.java` : six faces de lecture/émission
|
||||
redstone 0–15, comparateur, impulsions, inventaire d’entrée/sortie, reconnaissance
|
||||
d’items et transfert conditionnel vers des inventaires adjacents.
|
||||
- `block/entity/BusConnectorBlockEntity.java`, `IoExtenderBlockEntity.java` et
|
||||
`DisplayMatrixBlockEntity.java` : échanges cadencés, extensions d’entrées/sorties,
|
||||
affichages et adressage. Le mod dispose aussi d’un détecteur de pluie.
|
||||
- `src/client/java/fr/koka99cab/redstone_computer/gui/RedstoneComputerScreen.java` : édition du code et exemples de porte
|
||||
logique, filtre d’items, relais de bus et mémoire commandée par une horloge.
|
||||
- `network/PersonalComputerNetworking.java` : programmes enregistrés, catalogue
|
||||
public, contrôle à distance, gestion d’appareils associés et transfert de propriété.
|
||||
- `server/RedstoneComputerServer.java` : lecture de collection, advancements et
|
||||
statistiques personnelles pour alimenter des programmes.
|
||||
- `server/ComputerNetworkService.java` : appareils liés par identifiant et code,
|
||||
consultation de leur état et possibilité de garder leurs chunks chargés.
|
||||
|
||||
Le langage lit des ports et appelle les opérations définies par son interface
|
||||
`Host`. Dans le code parcouru, il ne fournit pas une instruction d’exécution
|
||||
arbitraire de commandes opérateur. Les transferts déplacent des items existants ;
|
||||
ce n’est pas une machine de création libre de blocs.
|
||||
|
||||
## Points à reprendre autrement
|
||||
|
||||
**Le processeur n’est pas continu dans cette référence.** `RedstoneProgram.tick`
|
||||
crée un nouveau CPU et repart à l’instruction zéro à chaque tick. Registres,
|
||||
indicateurs et position d’exécution sont recréés. Le message de budget atteint
|
||||
n’implique pas une reprise à l’instruction suivante. La RAM, le programme,
|
||||
l’inventaire et les impulsions sont néanmoins sauvegardés par l’entité de bloc.
|
||||
|
||||
Le projet Sanctuary souhaite un ordinateur dont l’état complet peut être
|
||||
retrouvé. Cela demande de concevoir la continuité du processeur, ses attentes,
|
||||
ses arrêts et les opérations déjà effectuées, puis de les vérifier en sauvegarde
|
||||
et rechargement. La conservation de RAM seule ne suffit pas.
|
||||
|
||||
Les transferts sont limités jusqu’à 64 items par instruction, sans budget global
|
||||
de débit par machine. Les budgets de calcul historiques ne donnent donc pas à
|
||||
eux seuls un équilibre de production ou une garantie de performance du serveur.
|
||||
Un registre d’adresse doit aussi permettre d’exploiter la RAM étendue : un
|
||||
registre de données 8 bits seul ne couvre que 256 adresses en accès indirect.
|
||||
|
||||
L’ancien réseau autorise quatre appareils épinglés par profil et un plafond
|
||||
global de chunks forcés de 128 par défaut. Pour Sanctuary, le maintien en activité
|
||||
doit être raccordé au système **Chunky et clés de chunk** déjà prévu. Un programme
|
||||
ou un terminal réseau ne doit pas offrir un second accès gratuit à cette fonction.
|
||||
|
||||
Les connaissances et statistiques de l’ancien ordinateur font désormais doublon
|
||||
avec le Blocodex et le recensement Sanctuary : proposer des consommateurs de ce
|
||||
socle commun. Les compteurs exacts ne doivent pas être ramenés silencieusement à
|
||||
255 pour tenir dans un registre ; une représentation ou un protocole est à choisir.
|
||||
|
||||
Les tests historiques lus portent notamment sur les impulsions et les profils
|
||||
mémoire. Ils ne constituent pas une validation du futur CPU continu, des actions
|
||||
matérielles ou de la charge d’un serveur Sanctuary.
|
||||
|
||||
## Proposition : une grande puissance obtenue en survie
|
||||
|
||||
« Code légal » est compris ici comme une capacité très puissante construite et
|
||||
utilisée dans la partie, avec les ressources et déblocages du jeu. La puissance
|
||||
recherchée vient de ce que le joueur peut composer avec ses programmes.
|
||||
|
||||
Proposer un **socle autonome de calcul et de redstone programmable**, auquel
|
||||
Sanctuary raccorde ses fonctions particulières. Ce découpage est une proposition
|
||||
d’architecture, pas la création d’un nouveau mod ou d’identifiants.
|
||||
|
||||
| Pouvoir proposé | Effet intéressant | Condition matérielle ou de progression |
|
||||
| --- | --- | --- |
|
||||
| Circuits programmables | Horloges, mémoires, séquenceurs, serrures, ascenseurs, aiguillages et petits jeux. | Ports physiques, entrées réelles et exécution bornée. |
|
||||
| Production pilotée | Reconnaître, compter, trier, traiter des lots et coordonner des machines. | Inventaires et périphériques accessibles ; débit de transfert explicite. |
|
||||
| Programmes partageables | Récupérer, comprendre, adapter et transmettre un montage fonctionnel. | Code copiable, matériel à construire et autorisations propres à l’installation. |
|
||||
| Construction assistée — piste ultérieure | Produire un plan, puis commander sa réalisation avec des matériaux connus. | Appareil de placement à concevoir, vraie réserve de blocs, zone et cadence définies. Connaître un matériau ne le fournit pas. |
|
||||
| Installations spatiales Sanctuary | Préparer une expansion, établir une liaison ou piloter un accès à un espace. | Ancre, dépôt et autres appareils à définir ; destinations, ressources et déblocages validés côté serveur. |
|
||||
|
||||
La liberté du langage doit permettre de créer des montages imprévus. Les limites
|
||||
portent sur la mémoire, le temps de calcul et les capacités des périphériques.
|
||||
Un code long attend son prochain budget ; il ne recommence pas silencieusement
|
||||
au début. Le premier budget n’est pas fixé par cette lecture.
|
||||
|
||||
La copie d’un programme est une manière de partager un savoir. Les joueurs
|
||||
peuvent construire et utiliser des installations communes sans tous devenir
|
||||
programmeurs. Les ruines pourraient fournir des montages lisibles par leurs
|
||||
anciens usages : pompage, orientation, production, puis opérations spatiales.
|
||||
|
||||
Le galactique conserve son rôle de langage opératoire proposé dans le
|
||||
[cahier des machines](langage-et-machines.md). Les pouvoirs spatiaux demeurent
|
||||
soumis à leur progression, dont le déblocage collectif permanent des portails
|
||||
de minage. L’origine des glyphes et leur lien aux Endermen restent à écrire.
|
||||
|
||||
## Première preuve jouable proposée
|
||||
|
||||
Le socle Sanctuary retient désormais trois blocs : contrôleur à six faces avec
|
||||
RAM intégrée, terminal et afficheur, plus les disquettes. L’assemblage du code
|
||||
est une fonction du terminal. Un compteur d’impulsions pilotant une porte suffirait à éprouver
|
||||
la première version avec les blocs redstone ordinaires, sans périphérique de
|
||||
manipulation d’items. Programme et état seraient inspectables, sauvegardés et
|
||||
repris après rechargement. Un autre joueur pourrait réutiliser le code avec son
|
||||
propre matériel. Le tri et les autres périphériques restent des extensions.
|
||||
|
||||
Cette preuve testerait la continuité du CPU, l’utilité en survie et le partage
|
||||
avant les fonctions spatiales. Taille mémoire, coût, alimentation, débit et
|
||||
forme des blocs restent à concevoir ensemble. Aucune livraison n’est lancée
|
||||
par cette proposition.
|
||||
@@ -1,270 +0,0 @@
|
||||
# Redstone Language — fiches des composants
|
||||
|
||||
**Proposition de conception, WG-26 ; aucun bloc ni comportement livré.** Ces
|
||||
fiches complètent le [cahier du langage](langage-et-machines.md),
|
||||
l’[ISA proposée](redstone-language-instructions.md) et
|
||||
l’[audit redstone](audit-redstone-26.3.md). Les
|
||||
[extensions](redstone-language-extensions.md) et les
|
||||
[composants de signal et transport](redstone-language-signaux-et-transport.md)
|
||||
explorent les appareils supplémentaires. Ces fiches rendent le premier ensemble
|
||||
constructible et compréhensible en jeu. Les paragraphes **Retenu** rappellent
|
||||
les choix de l’auteur ; les gestes, valeurs, interfaces et règles de conservation
|
||||
marqués **Proposition** forment un contrat à éprouver, sans devenir du canon.
|
||||
|
||||
## Ensemble et règles communes
|
||||
|
||||
**Retenu :** trois blocs informatiques — contrôleur, terminal, afficheur — et
|
||||
une disquette de programme. Le particuleur est un appareil complémentaire,
|
||||
utilisable sans ordinateur. L’assemblage appartient au terminal ; processeur et
|
||||
RAM sont intégrés au contrôleur. Aucun bloc Assembleur, lecteur de disquette,
|
||||
module RAM ou bus de données supplémentaire n’est nécessaire à cet ensemble.
|
||||
|
||||
**Proposition commune :** les gestes portant sur les objets et la configuration
|
||||
aboutissent côté serveur. Un menu affiche l’état confirmé, pas un résultat
|
||||
anticipé. Les blocs posés conservent leur orientation et ne tournent pas sous
|
||||
un clic de configuration. Pour les déplacer, on les récupère puis on les repose.
|
||||
Un objet tenu qui n’a pas d’usage propre sur l’appareil garde son interaction
|
||||
habituelle ; sneak avec un bloc permet notamment de construire contre lui.
|
||||
|
||||
La [clé à molette dorée](machines-multiblocs.md#clé-à-molette-dorée--assembler-désassembler-et-orienter)
|
||||
est retenue pour orienter des blocs, assembler volontairement et dissocier les multiblocs.
|
||||
Son application aux appareils de cette fiche reste à définir : elle serait
|
||||
une interaction explicite avec un outil tenu, distincte des clics de configuration
|
||||
main vide. Pour le contrôleur, tourner la façade ne remapperait ni les ports
|
||||
cardinaux, ni le programme ; l’état de travail serait conservé et les connexions
|
||||
revérifiées à l’arrêt ou en pause avant reprise.
|
||||
|
||||
| Matière et déplacement — propositions | Comportement du premier ensemble |
|
||||
| --- | --- |
|
||||
| Eau | Blocs non waterloggables, pose en cellule sèche ; aucun bonus, court-circuit ou destruction électrique simulé. L’eau n’entre pas dans leur volume. |
|
||||
| Pistons | Les quatre types de blocs ne sont ni poussés ni tirés ; pas de copie de leur état par déplacement. |
|
||||
| Support | Contrôleur et particuleur sont des blocs pleins ordinaires. Terminal et afficheur se fixent sur une face verticale solide ; perdre ce support provoque leur dépose. Les composants d’un écran doivent donc avoir chacun un support. |
|
||||
| Récupération | Proposition : pioche pour récupérer l’appareil ; ses objets internes sont conservés ou rendus une seule fois selon les fiches ci-dessous. Pas d’obligation de Silk Touch pour retrouver un programme. |
|
||||
| Destruction sans récupération | Aucune seconde machine ni sauvegarde portable n’est créée. Une éventuelle restitution Sanctuary appartient à son contrat propre. |
|
||||
| Indices | Faces, prises, fentes et témoins intégrés permettent de reconnaître entrées, sorties, marche et erreur. Couleur et forme se complètent ; aucun panneau explicatif généré n’est nécessaire. |
|
||||
|
||||
Les entrées redstone lisent ce qui arrive réellement au bloc. Les sorties ne
|
||||
sont pas une alimentation forte omnidirectionnelle gratuite : seuls les ports
|
||||
prévus émettent, selon un raccord redstone à vérifier sur le jeu exact.
|
||||
Les lectures de périphériques et les transferts d’objets ne sont pas des
|
||||
intensités électriques cachées.
|
||||
|
||||
## Contrôleur — la machine qui continue
|
||||
|
||||
**Retenu :** assembleur direct, six faces d’entrée/sortie, RAM adressable et
|
||||
fonctionnement autonome après retrait du terminal.
|
||||
|
||||
**Profil d’essai proposé :** 1 Kio de RAM, quatre registres de 8 bits et au plus
|
||||
64 instructions exécutées par tick serveur. Code et RAM sont distincts ; les
|
||||
adresses doivent couvrir la RAM entière. Ce sont des valeurs de prototype,
|
||||
pas des caractéristiques déjà validées. Le détail des instructions appartient
|
||||
à l’[ISA proposée](redstone-language-instructions.md).
|
||||
|
||||
| Fiche — propositions | Comportement |
|
||||
| --- | --- |
|
||||
| Forme et placement | Petit bloc technique plein, façade de repérage tournée vers le joueur. Les six ports restent identifiables sur toutes les faces. Les adresses physiques suivent les directions du monde ; changer la pose ne remappe pas silencieusement le programme. |
|
||||
| Clic droit, main vide | Ouvre la configuration locale des faces et un résumé de marche ; l’édition du programme se fait au terminal. |
|
||||
| Sneak + clic droit, main vide | Ouvre directement la fiche de la face visée. Aucun changement de mode au simple toucher. |
|
||||
| Objet tenu | La disquette ne se charge pas directement dans ce bloc : elle utilise le terminal. Poser un terminal ou un afficheur contre une face reste un geste de construction. |
|
||||
| Entrées/sorties | Chaque face prend exactement un mode : **OFF**, **IN**, **OUT** ou **DEVICE**. IN lit l’intensité 0–15 réellement reçue par cette face. OUT fournit seulement un signal faible 0–15 sur cette face, jamais un signal fort ; les autres faces ne relaient pas sa valeur. DEVICE adresse un appareil adjacent compatible et n’émet pas simultanément de redstone. OFF ne fait ni l’un ni l’autre. |
|
||||
| Réglage des faces | Machine arrêtée ou en pause ; le changement désactive d’abord les sorties concernées. Les faces non configurées sont OFF. Un périphérique manquant donne un état indisponible, sans charger un chunk pour le chercher. |
|
||||
| Comparateur et inventaire | Pas de sortie de comparateur dédiée ni d’inventaire d’objets. RAM occupée et valeur du dernier port ne sont pas un remplissage de coffre. |
|
||||
| Indices | Port rentrant, sortant ou prise distincte ; témoins de marche, pause et erreur. Le terminal fournit le détail utile au dépannage. |
|
||||
|
||||
**Proposition d’état :** le contrôleur conserve le document de programme
|
||||
(source, bytecode correspondant, version et crédits), la RAM, les registres,
|
||||
le compteur d’instruction, la pile d’appels, les indicateurs, l’attente, les
|
||||
valeurs mémorisées des sorties et la configuration des faces. HALT, pause et
|
||||
erreur mettent immédiatement les sorties électriques à zéro. Une reprise
|
||||
explicite ne réémet pas une ancienne valeur avant le calcul
|
||||
prévu par le programme. Arrêter le CPU n’annule pas une opération serveur déjà
|
||||
engagée par une installation : son journal conserve son autorité.
|
||||
|
||||
Un rechargement de la machine en place reprend son état sauvegardé : une machine
|
||||
en marche continue au compteur d’instruction conservé, une machine arrêtée
|
||||
reste arrêtée. Pour la machine en marche, les valeurs mémorisées des sorties
|
||||
sont restaurées, puis modes de ports, connexions et entrées sont relus avant
|
||||
leur réémission sur les faces OUT valides. Le CPU ne repart pas au début et
|
||||
ne rejoue pas une instruction pour reconstruire ces valeurs. Il n’y a pas de
|
||||
rattrapage des ticks pendant lesquels le chunk ou le serveur n’exécutait pas
|
||||
la machine.
|
||||
|
||||
**Casse et récupération proposées :** couper les sorties, puis transférer une
|
||||
seule fois le document et l’état de travail dans l’objet machine, marqué
|
||||
**STOPPED**, avec RAM, compteur d’instruction et pile conservés. Sa repose
|
||||
demande une reprise explicite au terminal pour continuer à cet endroit. Une
|
||||
relance distincte repart au début du même programme, remet le CPU à l’état
|
||||
initial et conserve la RAM. L’appareil ne démarre pas seul dans un coffre,
|
||||
une main ou une nouvelle installation. L’export sur disquette ne copie pas
|
||||
cet état de travail.
|
||||
|
||||
## Terminal — écrire et examiner
|
||||
|
||||
**Retenu :** éditeur d’assembleur, vérification, assemblage, lecture/écriture des
|
||||
disquettes, inspection et commandes d’exécution. Il n’exécute pas le CPU à la
|
||||
place du contrôleur et n’est pas nécessaire à son fonctionnement continu.
|
||||
|
||||
| Fiche — propositions | Comportement |
|
||||
| --- | --- |
|
||||
| Placement | Console fixée contre un contrôleur : son dos touche une face DEVICE et sa façade reste accessible. Un terminal correspond à ce seul contrôleur adjacent ; aucun réseau distant implicite. |
|
||||
| Clic droit, main vide | Ouvre l’éditeur et les vues programme, RAM, registres et ports. Sans connexion valide : écran déconnecté, sans machine locale de remplacement. |
|
||||
| Sneak + clic droit, main vide | Retire la disquette présente, rendue dans l’inventaire ou déposée au sol si nécessaire. |
|
||||
| Disquette tenue | Insère un seul support si la fente est libre ; une fente occupée ne détruit ni ne remplace automatiquement son contenu. Le menu propose ensuite lire, charger ou enregistrer. |
|
||||
| Autres entrées/sorties | Dialogue avec le contrôleur par son dos DEVICE ; aucune émission redstone ni alimentation obligatoire pour l’éditeur dans ce prototype. |
|
||||
| Comparateur | Proposition : 0 sans disquette, 15 avec disquette. Cela n’indique ni validité du code ni avancement du CPU. |
|
||||
| Automatisation | Insertion d’une disquette par dessus ou côtés libres, extraction par dessous. La façade et le dos de connexion n’exposent pas la fente. Un hopper change le support présent, jamais le programme chargé. |
|
||||
|
||||
**Une seule source de travail proposée :** le contrôleur possède le document
|
||||
éditable. Le terminal en présente une vue ; il ne garde pas une deuxième copie
|
||||
persistante indépendante. Sans contrôleur, il ne propose donc pas d’éditeur
|
||||
autonome. Plusieurs vues éventuelles utilisent la révision du même document,
|
||||
sans écraser silencieusement une modification concurrente.
|
||||
|
||||
Éditer ou assembler demande un CPU arrêté ou en pause ; charger un autre
|
||||
programme exige l’état arrêté. Les modifications
|
||||
forment une révision source marquée non assemblée : tant que le bytecode de
|
||||
cette révision n’est pas validé, elle ne peut pas être exécutée ou exportée
|
||||
comme programme prêt. Un assemblage raté laisse les données de travail intactes
|
||||
et les sorties arrêtées. Charger un autre programme remplace explicitement le
|
||||
document, remet RAM, registres et exécution à leur état initial et laisse le
|
||||
CPU arrêté : la RAM est alors entièrement à zéro. Relancer le même programme
|
||||
garde sa RAM ; reprendre une pause conserve aussi le compteur d’instruction,
|
||||
la pile et l’attente.
|
||||
|
||||
**Retrait proposé :** casser seulement le terminal ne détruit ni n’arrête le
|
||||
contrôleur. Le terminal tombe comme appareil vide ; la disquette tombe séparément
|
||||
une seule fois. Le contenu de l’éditeur reste dans le contrôleur. Une fente
|
||||
visuellement occupée, le petit écran et l’indication de connexion suffisent à
|
||||
rendre l’usage lisible.
|
||||
|
||||
## Afficheur — un écran construit
|
||||
|
||||
**Retenu :** panneaux coplanaires de même orientation, rectangle plein, raccord
|
||||
visuel CTM et **huit lignes par bloc en hauteur**. Le texte traverse les joints ;
|
||||
ce n’est pas une copie du texte sur chaque panneau. L’écran tactile n’appartient
|
||||
pas au premier ensemble.
|
||||
|
||||
**Valeurs d’essai proposées :** au plus **8 × 4 panneaux**, avec **16 colonnes
|
||||
logiques par bloc**. Un écran maximal aurait 128 colonnes et 32 lignes. La grille
|
||||
doit être essayée avec les glyphes galactiques, les accents et les traductions.
|
||||
Un glyphe large peut occuper deux cases : seize colonnes ne garantissent pas
|
||||
seize caractères de toute écriture. La prise en charge des glyphes CJK reste
|
||||
à vérifier avec le rendu, sans promettre une police déjà disponible.
|
||||
|
||||
| Fiche — propositions | Comportement |
|
||||
| --- | --- |
|
||||
| Pose et orientation | Panneau sur face verticale solide, avant tourné vers la pièce. Raccord seulement entre côtés de même plan et même orientation ; pas autour d’un angle. |
|
||||
| Clic droit, main vide | Ouvre la configuration de l’écran : texte autonome, entrée analogique ou pilotage DEVICE. Ce menu de configuration n’en fait pas un écran tactile programmable. |
|
||||
| Sneak + clic droit, main vide | Montre l’ancre, les dimensions et les panneaux en attente, sans changer de groupe ni effacer le texte. |
|
||||
| Objet tenu | Aucun inventaire. Un panneau tenu se pose normalement ; pas de disquette directement dans la surface. |
|
||||
| Connexion | Un seul contrôleur pilote le rectangle, par une face DEVICE touchant le dos d’un de ses panneaux. L’extension de bus propose une prise de données pleine qui assure à la fois support et connexion distante. Une seconde connexion ne remplace pas automatiquement la première. |
|
||||
| Modes autonomes | Texte statique conservé ou lecture 0–15 au dos d’un panneau d’entrée désigné, pour nombre, jauge ou sélection de texte. Le mode DEVICE désactive cette entrée analogique. |
|
||||
| Sorties et comparateur | Aucun signal redstone, aucune sortie de comparateur et aucun transfert d’objets. Un contenu chiffré n’est pas une mesure électrique envoyée aux voisins. |
|
||||
|
||||
Le contrôleur étant un bloc plein à face solide, il peut servir directement de
|
||||
support au panneau connecté : sa face DEVICE touche le dos de ce panneau.
|
||||
Les autres panneaux reposent sur les blocs solides voisins. Aucun câble ou
|
||||
bloc de support intermédiaire ne s’insère entre la prise et le panneau ; un
|
||||
terminal utilise une autre face DEVICE libre du contrôleur.
|
||||
|
||||
**Construction progressive proposée :** un premier panneau crée une ancre 1 × 1.
|
||||
Des panneaux vierges ajoutés au bord sont adoptés comme extensions ; l’ancien
|
||||
rectangle reste actif pendant une rangée incomplète. L’agrandissement cherche
|
||||
localement le plus grand rectangle plein contenant toute la surface active,
|
||||
dans la limite d’essai. À égalité, largeur puis hauteur donnent un choix stable.
|
||||
Il ne déplace pas l’ancre et ne réorganise pas l’écran à chaque chargement.
|
||||
Deux écrans déjà configurés restent distincts : leur simple contact ne fusionne
|
||||
ni leurs textes ni leurs contrôleurs.
|
||||
|
||||
**Conservation proposée :** seule l’ancre possède le texte et la configuration ;
|
||||
les autres panneaux conservent une référence de groupe. Déconnexion ou pause du
|
||||
contrôleur fige la dernière image avec un indice de liaison interrompue. Elle
|
||||
ne transforme pas les panneaux en copies autonomes du programme.
|
||||
|
||||
Casser un panneau invalide la surface s’il manque une case : affichage du groupe
|
||||
suspendu, contenu intégral conservé dans l’ancre. Les morceaux ne deviennent
|
||||
pas autant d’écrans configurés. Recompléter le rectangle réactive le groupe.
|
||||
Si l’ancre est récupérée, **un seul panneau-objet** transporte sa configuration
|
||||
et son contenu ; les panneaux restants deviennent orphelins, sans texte dupliqué.
|
||||
Reposer cette ancre au raccord attendu peut restaurer le groupe. Une remise à
|
||||
zéro explicite permet de réutiliser un panneau orphelin comme panneau vierge.
|
||||
Une réduction volontaire de surface masque le débordement sans supprimer le
|
||||
contenu conservé ; aucun texte n’est perdu par simple redimensionnement.
|
||||
|
||||
## Disquette — transporter un programme
|
||||
|
||||
**Retenu :** support physique partageable, copiable et revendable, utilisable
|
||||
au terminal. La lecture du programme permet d’étudier et de reprendre un montage.
|
||||
|
||||
**Proposition :** une disquette contient un programme avec source, bytecode
|
||||
correspondant, version de format et de langage, nom et crédits. Elle ne transporte
|
||||
ni RAM, ni registres, ni état d’attente, ni autorisation d’achat ou d’expansion.
|
||||
La capacité d’essai est de **16 Kio de source UTF-8 et 512 instructions au plus**,
|
||||
en accord avec l’[ISA proposée](redstone-language-instructions.md). Un dépassement
|
||||
refuse l’enregistrement sans tronquer le programme ; son nom ne remplace pas
|
||||
son format ni sa validation.
|
||||
|
||||
- Clic droit sur le terminal : insertion ; main vide sur le terminal : menu ;
|
||||
sneak + main vide : retrait. Sur les autres blocs, la disquette ne lance rien.
|
||||
- Lire affiche le contenu sans modifier le contrôleur. Charger exige un CPU
|
||||
arrêté et importe un programme valide dans son document de travail. Le support
|
||||
peut ensuite être retiré : le contrôleur garde son programme.
|
||||
- Enregistrer exporte la révision assemblée du contrôleur vers une disquette
|
||||
vierge, ou remplace explicitement le contenu du support inséré. L’insertion
|
||||
d’un disque enregistré ne constitue jamais à elle seule une demande d’effacement.
|
||||
- Copier utilise le même parcours : importer, retirer l’original, insérer un
|
||||
support vierge, enregistrer. La copie emploie réellement ce support ; elle ne
|
||||
clone pas la machine. Crédits d’origine et adaptation restent distingués.
|
||||
- Format inconnu ou code invalide : lecture signalée comme impossible, sans
|
||||
vider le support ou la machine. Un programme reçu n’obtient aucun droit nouveau.
|
||||
|
||||
La disquette est un objet rangeable et transportable par les conteneurs usuels,
|
||||
sans comportement propre de piston, d’eau ou de comparateur. Hors terminal,
|
||||
elle suit les règles d’objet au sol et de destruction du jeu. Le passage dans
|
||||
un hopper ne l’exécute pas. Une étiquette lisible et une différence entre vierge
|
||||
et enregistrée suffisent au premier aspect visuel.
|
||||
|
||||
## Particuleur — choisir un effet, régler son débit
|
||||
|
||||
**Retenu :** clic droit avec un objet pour choisir le type de particules,
|
||||
insertion par dropper et débit lié au signal : **0 arrête ; 1–15 émet de plus
|
||||
en plus**. Le contrôleur peut commander cette redstone ordinaire, sans être
|
||||
nécessaire au particuleur.
|
||||
|
||||
| Fiche — propositions | Comportement |
|
||||
| --- | --- |
|
||||
| Corps et pose | Bloc plein avec bouche visible, orientable suivant les six directions. Il projette depuis cette bouche, pas depuis un voisin invisible. |
|
||||
| Emplacement | Un seul objet-échantillon conservé et non consommé par les émissions. C’est une proposition : l’ancienne version l’utilisait ainsi, mais la consommation n’est pas encore tranchée pour la reprise. |
|
||||
| Clic avec objet | Insère un objet, ou remplace l’échantillon en rendant le précédent. Le même objet déjà présent ne consomme pas une seconde unité. |
|
||||
| Main vide | Clic droit retire l’échantillon ; sneak + clic droit fait le même retrait, sans changer la direction. |
|
||||
| Automatisation | Dropper ou hopper vers le dessus, les côtés ou l’arrière pour remplir l’emplacement vide ; extraction par dessous. La bouche n’expose jamais d’inventaire : orientée vers le bas, elle empêche donc cette extraction inférieure. Pour un changement automatique d’effet, choisir une autre orientation. |
|
||||
| Redstone | Lire le meilleur signal reçu 0–15 ; ne rien émettre électriquement et ne pas alimenter fortement les voisins. La bouche reste un débouché visuel, pas une sortie redstone. |
|
||||
| Débit d’essai | Exemple à tester : **2 × signal particules par seconde**, réparties dans le temps. Ce n’est pas une valeur retenue ; taille, vitesse et portée ne croissent pas automatiquement avec le signal. |
|
||||
| Vide ou objet sans effet | Aucune émission ; indice discret de fente vide ou d’échantillon sans correspondance. Pas de nuage de remplacement implicite. |
|
||||
| Comparateur | 0 vide, 15 occupé, même si l’échantillon n’a pas d’effet reconnu. Il indique une présence, pas le type de particule ni le débit. |
|
||||
| Récupération | Appareil et échantillon rendus séparément, une seule fois. Le bloc récupéré ne garde pas une seconde copie de l’objet inséré. |
|
||||
|
||||
**Table proposée :** associer les objets à des familles d’effets compréhensibles,
|
||||
par exemple charbon de bois/fumée, bloc musical/notes, mousse/spores. La table
|
||||
exacte et les objets moddés restent à choisir ; une particule de flamme ne met
|
||||
pas le feu et une particule d’eau ne crée pas de fluide. Un budget d’émission et
|
||||
des réglages visuels permettront de maîtriser le coût de plusieurs appareils.
|
||||
|
||||
Une sortie du contrôleur règle le débit ; une autre peut déclencher le dropper
|
||||
ou verrouiller un hopper. Changer l’échantillon exige toujours un transfert
|
||||
physique réussi. Une future interface d’inventaire pourrait l’effectuer avec
|
||||
son propre contrat ; le signal 0–15 n’encode pas une identité d’objet.
|
||||
|
||||
## Trois montages pour éprouver l’ensemble
|
||||
|
||||
| Montage proposé | Ce que l’on comprend en observant |
|
||||
| --- | --- |
|
||||
| Réserve régulée | Coffre et comparateur vers IN ; contrôleur vers verrou de hopper ; afficheur connecté montrant la mesure 0–15. Retirer le terminal ne coupe pas la régulation. |
|
||||
| Atelier visible | Contrôleur produisant une sortie variable vers un particuleur à fumée. La quantité suit l’activité calculée ; l’échantillon détermine l’apparence. |
|
||||
| Programme transmis | Un montage fonctionne ; son terminal permet de lire le code, d’enregistrer une disquette et de charger un second contrôleur arrêté. Le deuxième reçoit le programme, pas le stock, la RAM ou les droits du premier. |
|
||||
|
||||
Ces fiches n’ajoutent aucune recette définitive ni identifiant de registre.
|
||||
Pistes matérielles seulement : pierre lisse et cuivre pour les boîtiers, redstone
|
||||
pour les circuits, verre pour les surfaces, papier et métal pour les supports.
|
||||
Quantités, coûts, progression, textures et compatibilité restent à éprouver dans
|
||||
le ticket d’implémentation.
|
||||
@@ -1,476 +0,0 @@
|
||||
# Redstone Language — ensemble et nouveaux composants
|
||||
|
||||
**Proposition V0.1, WG-26, 11 septembre 2026. Documentation, sans implémentation.**
|
||||
Le socle retenu reste contrôleur, terminal, afficheur et disquettes, complété
|
||||
par le particuleur. Ce socle n’interdit pas d’inventer d’autres blocs : **un
|
||||
besoin que le montage ne sait pas résoudre peut révéler un composant manquant.**
|
||||
Les nouveaux noms et les valeurs ci-dessous constituent une proposition à
|
||||
examiner ensemble, pas des fonctionnalités déjà validées ou livrées.
|
||||
|
||||
Le dossier forme désormais un ensemble de travail :
|
||||
|
||||
| Partie | Contrat proposé |
|
||||
| --- | --- |
|
||||
| [Instructions](redstone-language-instructions.md) | Grammaire, registres, mémoire, calcul, branchements, ticks, signaux, échanges périphériques, erreurs et reprise. |
|
||||
| [Composants du socle](redstone-language-composants.md) | Placement, clics, six faces, disquettes, écran rectangulaire, inventaires, casse et conservation. |
|
||||
| [Signal et transport](redstone-language-signaux-et-transport.md) | Condensateur retenu ; convoyeur en cuir à vitesse unique, posé comme un rail, arrêté sans redstone. Inversion à choisir ; moteur optionnel proposé. |
|
||||
| [Objets, outils et cristaux](redstone-language-objets-et-cristaux.md) | Exploration ouverte d’appareils ; rôle des objets insérés et phénomènes physiques. Tâches locales des villageois payées en émeraudes. |
|
||||
| [Machines multiblocs](machines-multiblocs.md) | 27 fours pour le grand four partagé avec It's Alive ! (nom proposé : Fourneau) ; Fût de 27 barils ; grand baril de fermentation ; collections visibles et méga-pistons. Trémie à 45 cases ; Carillon par assemblage adjacent choisi. Clé à molette dorée pour orienter, assembler volontairement et dissocier ; blocs indépendants à la pose. Métablit et autres pistes restent à explorer. |
|
||||
| Ce document | Nouveaux composants révélés par les besoins, connexions, profils de données et montages. |
|
||||
| [Audit vanilla](audit-redstone-26.3.md) | Ce que Minecraft Java 26.3-pre-2 fournit effectivement ; comparaison avec les intentions Sanctuary. |
|
||||
| [Langage et Galactium](langage-et-machines.md) | Direction culturelle, exploration, progression et installations spatiales. |
|
||||
|
||||
Les règles V0.1 précisent les anciennes pistes ouvertes ; leurs chiffres restent
|
||||
un profil d’essai. Elles ne sont ni une compatibilité avec l’ancien assembleur,
|
||||
ni une nouvelle version du pack.
|
||||
|
||||
## Trouver le bloc derrière le manque
|
||||
|
||||
Le programme calcule. Un capteur rend une donnée accessible. Un actionneur
|
||||
effectue une opération physique. La construction relie ces capacités. Ajouter
|
||||
un bloc se justifie quand il apporte une opération distincte, lisible dans le
|
||||
monde et réutilisable dans plusieurs montages.
|
||||
|
||||
| Besoin du joueur | Réponse existante ou trou | Proposition |
|
||||
| --- | --- | --- |
|
||||
| Savoir si un coffre est presque plein | Un comparateur suffit pour son remplissage 0–15. | Aucun nouveau bloc nécessaire. |
|
||||
| Connaître le nombre exact de lingots de fer | Le remplissage ne donne ni l’identité ni la quantité exacte. | **Lecteur de stock** : instrument de mesure accolé au conteneur. |
|
||||
| Déplacer des objets en continu | Hoppers, droppers et transports vanilla. | Les conserver comme premier transport. |
|
||||
| Choisir au cours du programme quel objet part dans quelle direction | Il faut un point physique de sélection, un stock intermédiaire et un résultat de transfert. | **Aiguilleur** : petit répartiteur d’objets. |
|
||||
| Calculer, mémoriser, temporiser | Circuits vanilla ; contrôleur pour compacter et reprogrammer. | Pas d’additionneur, de RAM externe ou d’assembleur obligatoire. |
|
||||
| Faire monter et redescendre progressivement un signal | Un petit instrument peut exprimer ce comportement sans programme. | **Condensateur** : charge analogique, sans nouveau système d’énergie imposé. |
|
||||
| Déplacer les drops à découvert sur une chaîne | Le convoyeur en cuir se pose comme un rail, à vitesse unique, et s’arrête sans redstone. | **Convoyeur en cuir** : marche alimentée ; sens réglé à la pose ou au clic proposé. Un moteur optionnel pourrait séparer marche et inversion. |
|
||||
| Relier quatre commandes binaires à une intensité | Le contrôleur pourrait le calculer ; un petit composant électrique peut aussi le faire seul. | **Convertisseur binaire**, encodeur/décodeur 4 bits. |
|
||||
| Réagir à la pluie réellement reçue | La mesure doit avoir un lieu, une exposition et un sens. | **Pluviomètre**, distinct d’une lecture globale invisible. |
|
||||
| Programmer dimanche à midi | Compter des ticks ne donne pas le calendrier civil du serveur. | **Horloge de référence**. |
|
||||
| Relier plusieurs appareils au-delà des six voisins | La poudre transporte une intensité ; les octets nécessitent une liaison définie. | **Adresseur, fil et prise de données**, extension physique optionnelle. |
|
||||
| Afficher ou vendre quelque chose à un joueur précis | Une impulsion n’identifie pas son auteur et ne constitue pas un paiement. | **Comptoir**, point de transaction avec interaction explicite. |
|
||||
| Ouvrir une expansion depuis un programme | Une coordonnée calculée ne crée pas un lieu. | **Ancre spatiale**, interface matérielle d’une installation Galactium. |
|
||||
| Fabriquer un objet suivant une recette | Le crafter assure déjà une fabrication matérielle. | Le piloter ; pas de commande de création libre d’objets dans le CPU. |
|
||||
| Transformer une ressource, pomper un fluide, prospecter une roche | Certains usages spatiaux ou industriels demanderont d’autres opérations. | Des besoins à démontrer par un montage ; ne pas ajouter d’emblée un bloc universel qui fait tout. |
|
||||
|
||||
Les noms « lecteur », « aiguilleur », « horloge », « comptoir » et « ancre »
|
||||
expriment ici une fonction. Leurs noms définitifs, éventuellement galactiques,
|
||||
leur recette et leur place dans la progression restent à choisir.
|
||||
|
||||
## Règles communes proposées
|
||||
|
||||
Les fiches suivantes proposent des blocs pleins, non waterloggables, récupérables
|
||||
à la pioche et immobiles aux pistons pour ce prototype. Leur façade est visible ;
|
||||
la pose fixe une orientation horizontale. Les indications avant/arrière/gauche/
|
||||
droite désignent cette orientation du bloc ; les programmes adressent toujours
|
||||
les faces cardinales du contrôleur. Le terminal permet de voir la correspondance.
|
||||
|
||||
Un clic droit main vide ouvre une configuration, jamais une exécution cachée.
|
||||
Sneak avec un bloc tenu permet de construire contre l’appareil. Un objet sans
|
||||
interaction décrite conserve son usage normal. Les réglages appartiennent au
|
||||
bloc, les résultats réels au serveur. Une casse rend le bloc et son inventaire
|
||||
une seule fois ; une donnée de mesure ne devient pas un objet supplémentaire.
|
||||
|
||||
Les sorties de signal proposées sont **faibles et limitées à leurs faces
|
||||
désignées**, sans alimentation forte des voisins. Une prise DEVICE ne transporte
|
||||
ni objets ni énergie. Une face ne cumule pas entrée électrique, sortie électrique
|
||||
et prise DEVICE. Il n’y a pas de chargement de chunks induit par la connexion.
|
||||
|
||||
La mesure ou l’action ne travaille que lorsque les blocs nécessaires sont
|
||||
chargés et autorisés à fonctionner. Il faut traiter un voisin indisponible
|
||||
comme indisponible ; une mesure ancienne ne devient pas un zéro mesuré.
|
||||
|
||||
## Lecteur de stock
|
||||
|
||||
**Primitif : examiner un seul conteneur réellement accolé.** Un coffre double
|
||||
accessible constitue un inventaire logique ; un réseau de 128 coffres n’est
|
||||
pas découvert par ce petit bloc. Le terminal de stockage en titane garde son
|
||||
rôle distinct. Les adaptateurs d’inventaire devront respecter les faces et
|
||||
restrictions des conteneurs ; la fiche ne promet pas la compatibilité de tous
|
||||
les inventaires moddés.
|
||||
|
||||
| Fiche proposée | Comportement |
|
||||
| --- | --- |
|
||||
| Faces | Avant : sonde vers le conteneur. Arrière : DEVICE. Gauche/droite : sortie faible de seuil. Dessus/dessous : sans fonction de transfert. |
|
||||
| Interaction | Main vide : choisir une case ou « tout le conteneur », un filtre et un seuil. Un objet tenu sert de modèle au filtre sans être consommé ; aucune case de stockage réelle. |
|
||||
| Filtre | Identité d’objet, avec option de correspondance exacte des composants. Sans filtre, compter toutes les unités ; ne pas confondre unités et stacks. |
|
||||
| Mesure | Acquisition périodique toutes les 4 ticks chargés, avec première acquisition au premier tick disponible. Le CPU peut figer le dernier relevé dans un instantané distinct. Conserver types, composants, quantités et révision de cet instantané jusqu’à la prochaine capture. Une lecture multi-octets ne mélange pas deux relevés. |
|
||||
| Usage sans CPU | Seuil de quantité configuré : sortie 15 si atteint, 0 sinon. Témoin distinct si aucune mesure valide ; les sorties reviennent alors à 0. |
|
||||
| Comparateur | Relayer le remplissage ordinaire 0–15 du conteneur quand disponible ; ne pas faire passer un nombre exact dans ce signal. |
|
||||
| DEVICE | Capturer le dernier relevé, consulter sa validité, sa révision, le total filtré et les lignes du stock. Aucun retrait ou insertion. |
|
||||
| Sauvegarde | Conserver filtre et seuil ; au rechargement marquer le relevé périmé jusqu’à une mesure réelle. |
|
||||
|
||||
Usages : arrêt d’une chaîne à 128 lingots, inventaire d’atelier, compteur de
|
||||
dons, recherche du matériau manquant. Le relevé n’établit pas qui possède un
|
||||
objet ou qui l’a déposé. Il ne réserve pas le stock : un hopper peut le modifier
|
||||
après sa lecture.
|
||||
|
||||
## Aiguilleur
|
||||
|
||||
**Primitif : choisir la sortie d’objets présents dans l’appareil.** Il sépare
|
||||
la manipulation de matière du CPU. Le programme peut être très puissant sans
|
||||
que chaque contrôleur soit en même temps un coffre et une pompe universelle.
|
||||
|
||||
| Fiche proposée | Comportement |
|
||||
| --- | --- |
|
||||
| Faces | Dessus : entrée hopper/dropper. Avant, gauche, droite, dessous : destinations matérielles. Arrière : DEVICE, ou entrée redstone dans le mode autonome, jamais les deux. |
|
||||
| Inventaire | 5 cases, limites de stack des objets conservées. Les objets entrent réellement ; aucun prélèvement distant dans un coffre. |
|
||||
| Interaction | Main vide : inventaire et filtres par sortie. Un filtre est un modèle de comparaison ; retirer un vrai objet exige l’interaction d’inventaire habituelle. |
|
||||
| Mode autonome | Une transition 0 → positif sur l’arrière demande le transfert d’une unité selon les filtres ; priorité avant, droite, gauche, dessous. Le signal maintenu ne recommence pas sans nouveau front. |
|
||||
| Mode DEVICE | Demander une sortie, une case source et une quantité de 1 à 64 ; figer aussi l’identité et les composants de l’objet alors présent. L’appareil renvoie une identité d’opération et le nombre effectivement déplacé de cet objet. |
|
||||
| Cadence | Proposition : une unité tous les 8 ticks chargés, au total pour l’appareil. Une commande de 64 unités organise une suite de transferts, pas un déplacement instantané de 64 stacks. |
|
||||
| Destination bloquée | Conserver l’objet ; ne rien jeter dans le vide. Terminer avec résultat partiel après 40 ticks chargés consécutifs sans insertion possible. |
|
||||
| Automatisation passive | Les sorties permettent aussi une extraction par hopper suivant leur filtre ; cette extraction modifie le vrai stock. L’opération active revérifie chaque unité, sans réserver une copie. Si la case devient vide ou contient un autre objet/composants, terminer avec le résultat partiel ; ne jamais poursuivre avec l’objet remplaçant. |
|
||||
| Comparateur | Remplissage ordinaire des 5 cases. Il ne représente pas l’avancement de la commande. |
|
||||
| Casse/reprise | Les unités déjà transférées restent transférées ; rendre le tampon restant, terminer l’opération interrompue et conserver son reçu. Déchargement : suspendre, sans rejouer les transferts terminés. |
|
||||
|
||||
Usages : commandes préparées par quantité, échantillons du particuleur, tri
|
||||
changeant selon le travail d’une usine, alimentation d’un crafter. Le programme
|
||||
choisit ; les inventaires voisins acceptent ou refusent. Le profil doit exposer
|
||||
un résultat même si le stock observé auparavant a changé.
|
||||
|
||||
## Convertisseur binaire
|
||||
|
||||
**Primitif : passer entre quatre états électriques et un nombre 0–15.** C’est
|
||||
un composant utilisable avec des leviers, des lampes ou des pistons, sans CPU.
|
||||
|
||||
Proposition : mode encodeur ou décodeur choisi au clic dans la configuration.
|
||||
Avant porte la valeur analogique ; arrière, gauche, droite et dessus portent
|
||||
respectivement les bits de poids **1, 2, 4 et 8**. Dessous reçoit une horloge.
|
||||
Il n’y a pas de port DEVICE dans cette première fiche.
|
||||
|
||||
- **Encodeur** : relever les quatre entrées au front montant inférieur ; une
|
||||
entrée positive vaut 1. Publier leur somme sur l’avant et la conserver jusqu’au
|
||||
prochain front. Exemple : poids 1 et 4 actifs donnent 5.
|
||||
- **Décodeur** : relever l’intensité avant au front montant inférieur ; publier
|
||||
0 ou 15 sur les quatre sorties selon les bits, puis les conserver. Valeur 10 :
|
||||
seuls les poids 2 et 8 sont allumés.
|
||||
- Proposition de délai : mise à jour après 2 ticks serveur. Pas de mode transparent
|
||||
caché. Changer de mode met les sorties à zéro ; la valeur mémorisée en place
|
||||
est sauvegardée, mais un bloc récupéré se repose à zéro.
|
||||
- Comparateur : dernière valeur 0–15 mémorisée. Aucun inventaire.
|
||||
|
||||
Cette reprise simplifie le principe de l’extendeur historique, sans prétendre
|
||||
ajouter quatre canaux analogiques indépendants. Usages : code de porte à quatre
|
||||
leviers, sélection de gare, télécommande d’effets, lecture des bits d’un calcul.
|
||||
|
||||
## Pluviomètre
|
||||
|
||||
**Primitif : mesurer la pluie qui arrive ici.** Proposition distincte de l’ancien
|
||||
détecteur qui regardait la météo globale.
|
||||
|
||||
La coupelle supérieure doit être exposée à la précipitation du lieu. La neige
|
||||
ne compte pas comme pluie. Relevé toutes les 20 ticks chargés ; état binaire,
|
||||
sans inventer une intensité locale variable. Arrière DEVICE, autres faces
|
||||
horizontales sortie faible 0/15 ; dessus mesure et dessous support.
|
||||
|
||||
Main vide : inverser la sortie dans la configuration. Le relevé brut reste
|
||||
identique pour DEVICE. Comparateur : 15 quand le capteur reçoit de la pluie,
|
||||
0 sinon, indépendamment de l’inversion. Aucun inventaire, aucun prélèvement
|
||||
d’eau. Au rechargement, attendre un relevé avant d’annoncer un état valide.
|
||||
|
||||
Usages : toit automatique, affichage de météo locale, spectacle de particules,
|
||||
commande d’une installation extérieure. La météo globale et l’orage peuvent
|
||||
devenir d’autres informations, mais ne sont pas faussement présentés comme
|
||||
mesurés par cette coupelle.
|
||||
|
||||
## Horloge de référence
|
||||
|
||||
**Primitif : obtenir une date du serveur et déclencher un rendez-vous configuré.**
|
||||
Le chronomètre de machine reste un programme comptant ses ticks ; cette horloge
|
||||
apporte l’heure civile pour le temps réel de Sanctuary.
|
||||
|
||||
| Fiche proposée | Comportement |
|
||||
| --- | --- |
|
||||
| Faces | Arrière DEVICE ; avant sortie faible d’alarme ; autres faces sans émission. Aucun inventaire. |
|
||||
| Interaction | Clic : date locale du serveur, fuseau administrateur et calendrier d’alarme. Le joueur ne change pas l’horloge du serveur. |
|
||||
| Données | Date civile, instant UTC, jour de semaine et âge de cette partie. Distinguer âge de la partie, date historique de Minecraft et fondation de Sanctuary le 16 mars 2026. |
|
||||
| Mise à jour | Un relevé cohérent par seconde réelle quand l’appareil fonctionne ; aucune génération de tick de machine supplémentaire. |
|
||||
| Alarme | Règle unique jour/heure ; impulsion de 4 ticks chargés à la prochaine occurrence, identifiée par son instant UTC. Sauvegarder la dernière occurrence émise. |
|
||||
| Arrêt, retard, changement d’heure | Une occurrence entièrement passée hors fonctionnement est manquée. À l’automne, choisir la première occurrence d’une heure locale doublée ; au printemps, ignorer une heure locale inexistante. Un recul de l’horloge ne rejoue pas une occurrence déjà enregistrée. |
|
||||
| Comparateur | Quart de journée affiné en 16 positions : `floor(secondes_locales_du_jour × 16 / 86400)`, 0–15. Ce signal ne contient pas une date. |
|
||||
|
||||
Usages : calendrier public, ouverture du marché du dimanche, rendez-vous
|
||||
préécrits, affichage d’anniversaire. La programmation d’un horaire ne crée ni
|
||||
offre commerciale ni événement de progression par elle-même.
|
||||
|
||||
## Adresseur, fil et prise de données
|
||||
|
||||
**Primitif : choisir un appareil sur une liaison construite.** Cette extension
|
||||
évite de transformer la poudre en réseau invisible ou d’exiger un contrôleur
|
||||
collé derrière chaque mesure distante. Elle n’est pas requise pour une liaison
|
||||
DEVICE directe.
|
||||
|
||||
Proposition matérielle : un Adresseur plein, dos vers une face DEVICE du CPU,
|
||||
avant vers le fil ; les autres faces sont inactives. Un **fil de données** est
|
||||
un bloc de connexion fin à six branches, sans inventaire ni transfert d’items,
|
||||
sans émission redstone. Il peut relier une prise DEVICE exposée ; il n’occupe
|
||||
pas la même cellule que la poudre. Pose et récupération ne dupliquent pas
|
||||
les appareils raccordés. Il ne charge pas les chunks traversés.
|
||||
|
||||
Une **prise de données** pleine termine le fil : sa face avant DEVICE peut
|
||||
supporter un afficheur mural, ses autres faces reçoivent les branches du fil.
|
||||
Elle ne sélectionne pas une adresse et ne conserve pas le texte ; l’afficheur
|
||||
reste le périphérique. Le fil fin seul ne constitue pas un support d’écran.
|
||||
La prise reprend les règles de casse des blocs de connexion : aucune copie
|
||||
de données, perte du support entraînant la dépose du panneau. Le panneau peut
|
||||
toujours être posé directement sur un contrôleur pour le montage minimal.
|
||||
|
||||
Le réseau d’essai admet **64 segments, prises comprises, et 16 appareils**, dans une même dimension,
|
||||
avec **un seul Adresseur maître**. Les chemins cycliques sont parcourus une seule
|
||||
fois ; un dépassement ou deux maîtres met le réseau en conflit visible. Une
|
||||
rupture ne change pas les noms des appareils et ne réadresse pas leurs voisins.
|
||||
|
||||
Un clic sur chaque appareil lui attribue une adresse locale 1–16 ; doublon
|
||||
signalé, pas de choix aléatoire. Un clic sur l’Adresseur montre les adresses et
|
||||
leur disponibilité. Le CPU sélectionne une adresse dans son registre local,
|
||||
puis lit/écrit la fenêtre distante. Tous les accès distants voient le même
|
||||
budget CPU ; débit supplémentaire proposé de **16 octets par tick et par bus**,
|
||||
puis `BUSY`. Les accès sont courts, sans attente bloquante.
|
||||
|
||||
Comparateur de l’Adresseur : 15 réseau utilisable, 0 sans réseau ou en conflit.
|
||||
Le terminal précise la cause. Changer la sélection ne réexécute aucune commande.
|
||||
Déconnecter un appareil n’annule pas une opération qu’il a déjà acceptée.
|
||||
Le fil ne constitue ni une liaison interdimensionnelle, ni un tunnel gratuit
|
||||
pour les objets. Un fil isolé se récupère comme matériau, sans état d’exécution.
|
||||
|
||||
Usages : poste central d’atelier, plusieurs réserves, écrans éloignés, contrôle
|
||||
d’une construction étalée. Un réseau entre plusieurs CPU demandera un protocole
|
||||
supplémentaire : il n’est pas déduit de ce premier bus à maître unique.
|
||||
|
||||
## Comptoir
|
||||
|
||||
**Primitif : recevoir l’accord d’un participant identifié à une opération.**
|
||||
Un même bloc peut servir à une vente, un dépôt collectif ou une inscription.
|
||||
Il n’est pas un lecteur universel de l’identité des joueurs autour de lui.
|
||||
|
||||
Proposition : face avant interactive, arrière DEVICE. Dessus, dessous et côtés
|
||||
ne manipulent pas directement d’objets ; les coffres raccordés à l’installation
|
||||
portent les biens. Aucun comparateur révélant un portefeuille. Au clic droit,
|
||||
le joueur voit l’offre exacte et confirme. Le serveur produit alors un reçu
|
||||
opaque associé à cette offre, cette révision et ce joueur. Un hopper ou une
|
||||
impulsion ne peut pas remplacer ce geste.
|
||||
|
||||
L’offre préparée par le programme référence des biens réels, une quantité,
|
||||
un prix, une monnaie et une destination. Rubis comme monnaie de référence ;
|
||||
les autres monnaies ne sont admises que par une offre les acceptant explicitement.
|
||||
Pour ce profil d’essai : **une offre et une transaction active par comptoir** ;
|
||||
une file plus large peut être construite avec plusieurs comptoirs.
|
||||
|
||||
Le service commercial revérifie offre, stock, fonds et destinataire au moment
|
||||
de l’engagement. Il réserve puis termine la transaction avec un reçu durable.
|
||||
Une sortie électrique pourrait indiquer la réussite ; elle ne déclenche pas
|
||||
elle-même un second retrait de monnaie. En cas de casse ou de crash, le service
|
||||
termine ou restitue les réservations selon son journal ; il ne rembourse pas
|
||||
aveuglément un achat déjà livré. Le matériel exact des dépôts et portefeuilles
|
||||
dépend du contrat économique encore à concevoir.
|
||||
|
||||
Usages : boutique d’un joueur, retrait de commande, caisse d’événement, inscription
|
||||
à un concours. Une démonstration avant l’économie peut se limiter à l’échange
|
||||
d’items présents ; elle ne serait pas encore le Black Market de Sanctuary.
|
||||
|
||||
## Ancre spatiale
|
||||
|
||||
**Primitif : donner un point matériel de référence à un projet spatial.**
|
||||
L’ancre est une proposition distincte du noyau galactique : le noyau disparaît
|
||||
en libérant une charge collective ; aucune charge n’est enfermée dans cette
|
||||
nouvelle machine ni transportée dans son item.
|
||||
|
||||
Proposition : socle plein, marqueur d’orientation sur sa face supérieure,
|
||||
prise DEVICE à l’arrière. Clic main vide : installation reconnue, projet,
|
||||
conditions matérielles et opération courante. Aucun inventaire intégré ; des
|
||||
conteneurs raccordés apportent les ressources. Un comparateur peut indiquer
|
||||
0–15 d’avancement d’une opération, avec un témoin distinct pour attente/refus.
|
||||
|
||||
Le programme compose un projet puis demande une vérification : destination,
|
||||
climat, taille admissible, disponibilité spatiale, ressources et déblocages.
|
||||
La vérification n’engage pas une charge. Un lancement accepté réserve les
|
||||
ressources et une charge dans le journal du serveur, retourne l’identité de
|
||||
l’opération et poursuit le travail progressivement. Le même identifiant relu
|
||||
après interruption doit retrouver le même projet, pas une autre expansion.
|
||||
|
||||
La construction précise du multibloc et sa recette ne sont pas encore choisies.
|
||||
Cette fiche rend explicite **où** doit vivre son interface. Une copie de programme
|
||||
ne copie ni charge, ni réservation, ni progression collective. Retirer l’ancre
|
||||
cesse d’offrir le point de commande ; il ne fait pas disparaître un continent
|
||||
engagé ou terminé et ne restitue pas une charge consommée par défaut.
|
||||
|
||||
Usages proposés : ouverture de continent, référence d’un Indoor, installation
|
||||
de liaison. Chaque usage possède son propre contrat ; ils ne consomment pas
|
||||
automatiquement tous une charge d’expansion. Le déblocage collectif des portails
|
||||
des Cavernes reste issu du donjon prévu, pas d’un opcode qui contourne ce passage.
|
||||
|
||||
## Lire et écrire un périphérique
|
||||
|
||||
Le langage conserve deux instructions génériques : `PREAD` et `PWRITE`. Les
|
||||
appareils donnent un sens aux adresses ; il n’est pas nécessaire d’inventer
|
||||
`BUY`, `SUMMON`, `GIVE`, `SCANWORLD` ou une nouvelle instruction par boutique.
|
||||
|
||||
### Convention proposée de données
|
||||
|
||||
- Octets non signés ; valeurs multi-octets **petit-boutistes**, octet faible
|
||||
à l’adresse la plus basse. La paire d’adressage `[A:B]` du CPU reste haut/bas :
|
||||
il s’agit d’une notation d’adresse, pas d’un autre stockage de données.
|
||||
- `0x0000` lecture : version du profil, ici 1. `0x0001` lecture : type du profil.
|
||||
`0x0002` lecture : état local, 0 indisponible, 1 prêt, 2 occupé, 3 erreur.
|
||||
Autres adresses seulement si la fiche les expose ; sinon `BAD_ADDRESS`.
|
||||
- Types **provisoires de documentation** : 1 afficheur, 2 lecteur de stock,
|
||||
3 aiguilleur, 4 horloge, 5 pluviomètre, 6 adresseur, 7 comptoir, 8 ancre.
|
||||
Ni identifiants `sanctuary:*` ni numéros définitifs de sauvegarde.
|
||||
- `STATUS = 0` signifie lecture réussie ou écriture acceptée. Les états de
|
||||
tâche et reçus se lisent séparément. Après un refus de lecture, le registre
|
||||
destination conserve son ancienne valeur : vérifier `STATUS` avant de l’utiliser.
|
||||
- Une seule liaison pilote un appareil dans le prototype. Brancher simultanément
|
||||
un contrôleur direct et un bus rend la liaison conflictuelle ; pas de dernier
|
||||
écrivain gagnant silencieusement.
|
||||
|
||||
### Premier profil d’afficheur
|
||||
|
||||
L’écran fournit largeur en colonnes à `0x0010` et hauteur en lignes à `0x0011`.
|
||||
Un octet suffit avec le plafond proposé de 128 × 32. Le programme prépare une
|
||||
trame dans le tampon de l’appareil, puis la publie en une opération.
|
||||
|
||||
| Adresse | Accès et valeur proposés |
|
||||
| --- | --- |
|
||||
| `0x0020` | Écriture commande : 1 commencer une trame vide ; 2 publier ; 3 abandonner le tampon. Autre valeur refusée. |
|
||||
| `0x0021` | Écriture : ajouter un octet UTF-8 à la trame ouverte ; maximum 16 Kio. Lecture refusée. |
|
||||
| `0x0022` | Lecture : 0 aucun tampon, 1 trame en cours. |
|
||||
| `0x0023`–`0x0024` | Lecture : longueur du tampon sur 16 bits, figée tant qu’aucun octet n’est ajouté. |
|
||||
|
||||
Publier vérifie UTF-8 et la capacité ; une trame invalide conserve l’ancienne
|
||||
image et le tampon pour correction/abandon. Une trame publiée ferme le tampon.
|
||||
LF avance d’une ligne, CR est refusé ; le texte se replie à la largeur logique,
|
||||
le débordement vertical est conservé mais masqué comme dans la fiche d’écran.
|
||||
Une coupure de liaison abandonne la trame inachevée et garde la dernière image.
|
||||
Le premier profil transporte du texte ; couleurs, pictogrammes et édition d’une
|
||||
zone peuvent étendre le profil sans être prétendus disponibles dans ces octets.
|
||||
|
||||
Exemple proposé avec un afficheur au sud, face SOUTH en DEVICE :
|
||||
|
||||
```text
|
||||
PWRITE SOUTH, 0x0020, 1 ; commencer
|
||||
CMP STATUS, 0
|
||||
JNZ erreur
|
||||
PWRITE SOUTH, 0x0021, 79 ; O
|
||||
CMP STATUS, 0
|
||||
JNZ erreur
|
||||
PWRITE SOUTH, 0x0021, 75 ; K
|
||||
CMP STATUS, 0
|
||||
JNZ erreur
|
||||
PWRITE SOUTH, 0x0020, 2 ; publier « OK »
|
||||
CMP STATUS, 0
|
||||
JNZ erreur
|
||||
HALT
|
||||
erreur:
|
||||
HALT
|
||||
```
|
||||
|
||||
`HALT` coupe les sorties électriques du CPU ; l’image publiée reste dans
|
||||
l’afficheur. Cet exemple ne vérifie que les écritures acceptées, pas la présence
|
||||
d’un joueur devant l’écran.
|
||||
|
||||
### Relevés du stock, du temps et de la pluie
|
||||
|
||||
Pour ces trois instruments, `0x0010` est une commande d’instantané, écriture 1.
|
||||
`BUSY` signifie qu’aucun nouveau relevé n’a été accepté. La photographie publiée
|
||||
reste stable jusqu’à la suivante ; son âge et sa disponibilité restent distincts.
|
||||
Les octets de révision à `0x0011`–`0x0014` identifient ce relevé sur 32 bits.
|
||||
Les adresses `0x0015`–`0x0018` donnent son âge **au moment de la capture**, en
|
||||
ticks chargés u32, figé avec lui. `0x0019` vaut 1 si cet instantané reste
|
||||
utilisable, 0 si aucune capture n’existe ou si son instrument est indisponible.
|
||||
|
||||
| Profil | Données proposées |
|
||||
| --- | --- |
|
||||
| Stock | `0x0020`–`0x0023` quantité totale filtrée u32 ; `0x0024`–`0x0025` nombre de lignes u16. Sélecteur de ligne u16 écrit à `0x0030`–`0x0031`, validé par écriture 1 à `0x0032`. Ligne sélectionnée : quantité u32 à `0x0040`, longueur u16 à `0x0044`, identifiant UTF-8 à partir de `0x0100` (maximum 256 octets). Les composants exacts sont un jeton u32 à `0x0046`, local à cet instantané ; aucun numéro global d’item supposé stable. |
|
||||
| Horloge | Instant UTC en secondes u64 à `0x0020` ; année u16 à `0x0028`, mois/jour/heure/minute/seconde/jour de semaine aux octets `0x002A` à `0x002F`, lundi=1 ; âge de partie en jours u32 à `0x0030`. Les champs proviennent du même relevé et du fuseau serveur. |
|
||||
| Pluviomètre | Octet `0x0020` : 0 sec, 1 pluie reçue. Ne pas utiliser le relevé si l’état local dit indisponible. |
|
||||
|
||||
Les acquisitions périodiques alimentent les sorties autonomes et un relevé
|
||||
interne courant ; elles ne remplacent pas l’instantané détenu par le CPU.
|
||||
La commande capture le dernier relevé disponible de l’instrument. Elle ne
|
||||
force pas une inspection coûteuse pour chaque instruction du CPU. Les relevés
|
||||
stock et pluie sont invalidés quand le voisin ou l’exposition nécessaires ne
|
||||
peuvent plus être examinés. Un compteur dépassant u32 est signalé hors plage,
|
||||
jamais tronqué en petite quantité. Un consommateur relit la révision après une
|
||||
lecture longue ; si elle a changé, il recommence son relevé.
|
||||
|
||||
### Commandes matérielles et opérations longues
|
||||
|
||||
Pour l’aiguilleur, une préparation comporte case source, direction et quantité.
|
||||
Le comptoir prépare une offre ; l’ancre prépare un projet. Ces derniers schémas
|
||||
dépendent des systèmes économiques et spatiaux et ne reçoivent pas ici une
|
||||
fausse table binaire définitive. Ils doivent partager ce cycle :
|
||||
|
||||
1. Écrire une préparation dans l’appareil ; les écritures partielles ne font
|
||||
rien au monde. Valider sa forme et figer une révision.
|
||||
2. Demander l’engagement de cette révision avec une clé de requête persistante.
|
||||
Une révision déjà engagée renvoie la même opération, sans nouvel achat ni
|
||||
nouvelle expansion. Un refus ne consomme rien.
|
||||
3. Consulter `en attente / en cours / terminée / partielle / refusée / interrompue`
|
||||
et le reçu : quantité réellement déplacée, transaction ou projet.
|
||||
4. Acquitter le résultat pour libérer l’unique emplacement actif. Une nouvelle
|
||||
demande exige une nouvelle préparation ; réécrire « démarrer » en boucle ne
|
||||
crée pas d’opération neuve.
|
||||
|
||||
Proposition pour l’aiguilleur : case 0–4 à `0x0010`, direction 0 avant, 1 droite,
|
||||
2 gauche, 3 dessous à `0x0011`, quantité 1–64 à `0x0012`. Commande `0x0013` :
|
||||
1 figer, 2 engager, 3 acquitter, 4 abandonner une préparation non engagée.
|
||||
Identité d’opération u64 à `0x0020`, état à `0x0028` (0 vide, 1 préparée, 2 en cours,
|
||||
3 terminée, 4 partielle, 5 refusée, 6 interrompue), quantité transférée à `0x0029`.
|
||||
La préparation figée porte une identité attribuée et sauvegardée par l’appareil ;
|
||||
le CPU ne doit pas improviser une clé aléatoire à chaque tick. Le reçu est figé
|
||||
avant acquittement ; aucune nouvelle préparation pendant l’opération active.
|
||||
|
||||
Le journal de l’appareil ou du service garantit les reprises et l’absence de
|
||||
double engagement. **Sauvegarder la RAM seule ne garantit pas cela.** Les crashes
|
||||
entre mutation d’inventaire et écriture du reçu demandent un contrat de sauvegarde
|
||||
et des essais dédiés avant d’annoncer des transactions fiables. Ce dossier ne
|
||||
modifie aucun format de sauvegarde actuel.
|
||||
|
||||
### Fenêtre de l’Adresseur
|
||||
|
||||
Adresse distante 1–16 sélectionnée par écriture à `0x0010`. Fenêtre CPU
|
||||
`0x0100`–`0x80FF` : déduire `0x0100` pour obtenir l’adresse distante 0–`0x7FFF`.
|
||||
Le profil local de l’Adresseur reste lisible hors fenêtre. Une valeur d’adresse
|
||||
inconnue donne `NO_DEVICE` ; un conflit donne `DEVICE_ERROR` ; dépassement du
|
||||
débit donne `BUSY`, sans accepter l’accès. Pas d’emboîtement d’Adresseurs dans
|
||||
ce prototype. Tous les profils détaillés ci-dessus tiennent dans cette fenêtre.
|
||||
|
||||
## Ce que l’ancien mod apporte réellement
|
||||
|
||||
Lecture locale des registres et méthodes, sans essai de compatibilité 26.3 :
|
||||
|
||||
| Référence | Présence vérifiée | Rapport à cette proposition |
|
||||
| --- | --- | --- |
|
||||
| `Redstone/redstone_computer`, 2.0.0 pour 26.1.2 | `rain_detector`, `io_extender`, `bus_connector`, `display_matrix` et ordinateurs. | Pistes concrètes. Le capteur regarde la météo globale ; le bus sert surtout les matrices et n’est pas le protocole défini ici. |
|
||||
| `Redstone/adresseur`, 1.0.0 pour 26.1.2 | Initialiseur qui journalise, sans registre de bloc. | Nom disponible comme intention ; aucun fonctionnement ancien à présenter comme porté. |
|
||||
| `Redstone/drawer` et `26.2/redstoner` | Stockage mono-objet à compteur `long`. | Pas de compatibilité hopper établie dans les interfaces inspectées ; ne pas le mettre dans une chaîne comme déjà fonctionnel. |
|
||||
| `26.2/redstoner` | Particuleur, terminal de stockage jusqu’à 128 coffres/tonneaux, panneaux spécialisés et ordinateurs. | La reprise des capteurs exacts et des affichages peut s’en inspirer, sans restaurer tout l’ancien OS dans le terminal. |
|
||||
|
||||
Les ordinateurs historiques transfèrent aussi leurs propres cases d’entrée
|
||||
vers un inventaire voisin. Cela motive l’aiguilleur séparé ; ce code ne prouve
|
||||
pas une lecture universelle des coffres alentour. Les références sont conservées
|
||||
dans les dossiers voisins, jamais modifiées par ce chantier. Voir aussi la
|
||||
[lecture historique](redstone-computer-reference.md).
|
||||
|
||||
## Montages qui justifient les composants
|
||||
|
||||
| Installation proposée | Composition et expérience |
|
||||
| --- | --- |
|
||||
| Petite chaîne de production | Comparateur → contrôleur → crafter/dropper ; le lecteur devient utile seulement quand on veut une quantité exacte d’un matériau. |
|
||||
| Réserve industrielle | Lecteur → contrôleur → aiguilleur ; afficheur montrant commandes, stocks et quantités réellement livrées. |
|
||||
| Atelier spectaculaire | Aiguilleur changeant l’échantillon du particuleur ; puissance de redstone variant avec les étapes du programme. |
|
||||
| Galerie à quatre destinations | Convertisseur, quatre leviers et aiguillages construits ; contrôleur facultatif pour programmer une séquence. |
|
||||
| Marché du dimanche | Horloge → afficheur d’horaires ; comptoir pour les transactions réelles. Le prix et les règles du navet appartiennent à l’économie. |
|
||||
| Station d’observation | Pluviomètre et horloge reliés par Adresseur ; historique calculé dans la RAM, affiché dans le monde. |
|
||||
| Ancienne salle d’expansion | Programme trouvé sur disquette, ressources dans des conteneurs, contrôleur, ancre et afficheur de suivi ; installation reproductible ailleurs. |
|
||||
|
||||
Pour la première réalisation, proposer le socle informatique et le particuleur,
|
||||
avec **condensateur et convoyeur** comme premiers composants autonomes,
|
||||
puis **lecteur, aiguilleur et convertisseur** : on peut déjà mesurer, décider,
|
||||
agir et voir un résultat dans de nombreux montages. Horloge/pluie et réseau
|
||||
forment des extensions indépendantes. Comptoir et ancre attendent leurs services
|
||||
réels. Cet ordre organise le travail sans limiter définitivement l’ensemble.
|
||||
|
||||
La prochaine validation doit porter sur des constructions jouables : lecture
|
||||
exacte malgré les hoppers actifs, sortie bloquée sans perte, écran agrandi sans
|
||||
texte copié, signal réellement raccordable, heure d’été/hiver, interruption et
|
||||
reprise des opérations. Les recettes, textures, noms FR/EN, budgets et schémas
|
||||
de sauvegarde appartiendront aux tickets de code ; aucune livraison n’est
|
||||
prétendue dans ce cahier.
|
||||
@@ -1,354 +0,0 @@
|
||||
# Redstone Language — proposition d’instructions V0.1
|
||||
|
||||
**Cahier de conception, WG-26. Aucun interpréteur n’est livré ici.** La version
|
||||
V0.1 désigne cette proposition de langage, pas une version du mod Sanctuary.
|
||||
Les noms, nombres et comportements ci-dessous sont proposés ensemble pour
|
||||
obtenir un contrat vérifiable ; ils restent soumis à validation et à essais.
|
||||
|
||||
Les choix déjà retenus sont l’assembleur écrit directement, les six faces
|
||||
d’entrée/sortie, la RAM adressable et le socle contrôleur, terminal, afficheur
|
||||
et disquettes. Le bloc Assembleur est retiré : le terminal assemble le code.
|
||||
La sauvegarde complète du processeur demeure la base de travail proposée dans
|
||||
le [cahier des machines](langage-et-machines.md#architecture-du-contrôleur-à-définir).
|
||||
Ce document la précise ; il ne reprend pas implicitement les opcodes ni les
|
||||
limites du [Redstone Computer historique](redstone-computer-reference.md).
|
||||
|
||||
## Machine proposée
|
||||
|
||||
| Élément | Proposition V0.1 |
|
||||
| --- | --- |
|
||||
| Registres de données | `A`, `B`, `C`, `D` : quatre entiers non signés sur 8 bits, de 0 à 255. |
|
||||
| RAM | **1 Kio**, soit 1 024 cases d’un octet ; adresses valides de 0 à 1 023. Taille proposée pour l’équilibrage. |
|
||||
| Adresse | Entier sur 16 bits, fourni par un littéral ou une paire de registres ; cet espace d’adressage ne fournit pas 64 Kio de RAM. |
|
||||
| Programme | Au plus **512 instructions** et **16 Kio de source UTF-8** ; code séparé de la RAM, non modifiable par `ST`. Limites proposées. |
|
||||
| Exécution | Au plus **64 instructions par tick serveur et par contrôleur chargé**. Ce budget proposé ne promet pas un débit global sans essais de charge. |
|
||||
| Compteur d’instruction | `PC` interne, inspectable au terminal ; modifié par le déroulement, les sauts et les appels, pas par `MOV`. |
|
||||
| Pile d’appels | **16 adresses de retour**, séparées de la RAM. `SP` interne vaut de 0 à 16 ; aucun `PUSH`/`POP` de données dans cette version. |
|
||||
| Comparaison | Deux indicateurs `ZF` et `CF`, modifiés uniquement par `CMP` pendant l’exécution. |
|
||||
| Résultat périphérique | `STATUS`, octet en lecture seule pour le programme, distinct de `ZF` et `CF`. |
|
||||
| Faces | `NORTH`, `SOUTH`, `EAST`, `WEST`, `UP`, `DOWN`, selon les directions du monde ; pas d’alias dépendant de l’orientation du joueur. |
|
||||
|
||||
Un registre n’est pas un item et une case de RAM ne contient pas un bloc du
|
||||
monde. Ces octets décrivent un calcul ; seul un appareil explicitement relié
|
||||
peut interpréter certains résultats comme une demande d’action.
|
||||
|
||||
## Écriture et opérandes
|
||||
|
||||
Une instruction par ligne ; arguments séparés par des virgules. Un point-virgule
|
||||
introduit un commentaire jusqu’à la fin de la ligne. Une étiquette occupe une
|
||||
ligne sous la forme `boucle:`. Les lignes vides, commentaires et étiquettes ne
|
||||
consomment pas le budget d’instructions.
|
||||
|
||||
Les instructions, registres et faces sont insensibles à la casse ; les
|
||||
étiquettes sont sensibles à la casse, uniques, composées de lettres ASCII,
|
||||
chiffres et `_`, sans commencer par un chiffre ni reprendre un mot réservé.
|
||||
Les constantes sont décimales ou hexadécimales avec préfixe `0x` ; aucune
|
||||
conversion silencieuse d’une constante hors limites n’est autorisée.
|
||||
|
||||
| Notation dans les tableaux | Opérande autorisé |
|
||||
| --- | --- |
|
||||
| `r` | Un registre destination parmi `A`, `B`, `C`, `D`. |
|
||||
| `v`, `x`, `y` | Un registre de données, `STATUS`, ou un littéral de 0 à 255. |
|
||||
| `a` | Adresse de 0 à 65 535 écrite littéralement, ou paire `[A:B]`, `[B:C]`, etc. de deux registres de données distincts. |
|
||||
| `t` | Durée entière de 1 à 65 535 : littéral, registre de données ou paire de registres. Une valeur calculée égale à zéro est une erreur. |
|
||||
| `label` | Étiquette du programme ; pas de saut vers la RAM ni vers un nombre calculé. |
|
||||
| `face` | Une des six faces nommées ci-dessus. |
|
||||
|
||||
Dans `[A:B]`, `A` est l’octet haut et `B` l’octet bas : l’adresse vaut
|
||||
`256 × A + B`. Cette notation compose une adresse, elle ne lit pas à elle seule
|
||||
la RAM. Seules `LD` et `ST` accèdent à la mémoire ; pour les périphériques,
|
||||
l’adresse désigne un registre de l’appareil.
|
||||
|
||||
```text
|
||||
MOV A, 0x03
|
||||
MOV B, 0xFF
|
||||
MOV C, 42
|
||||
ST [A:B], C ; écrit 42 à l’adresse RAM 1023
|
||||
LD D, 1023 ; D vaut 42
|
||||
```
|
||||
|
||||
Un accès RAM à l’adresse 1024 ou au-delà échoue : il ne revient pas au début
|
||||
de la RAM. Pour un accès indirect dont un registre sert aussi de destination,
|
||||
tous les opérandes sont évalués avant l’écriture du résultat.
|
||||
|
||||
## Instructions du noyau
|
||||
|
||||
Toutes les instructions exécutées comptent pour une unité du budget, y compris
|
||||
un saut, `WAIT` ou une lecture périphérique. Une instruction atomique terminée
|
||||
avance `PC`, sauf si elle fixe une autre destination. Une erreur conserve la
|
||||
position de l’instruction fautive pour le diagnostic.
|
||||
|
||||
### Mémoire et calcul
|
||||
|
||||
| Instruction | Effet |
|
||||
| --- | --- |
|
||||
| `MOV r, v` | Copie la valeur dans le registre. |
|
||||
| `LD r, a` | Charge l’octet de RAM à cette adresse. |
|
||||
| `ST a, v` | Écrit un octet à cette adresse de RAM. |
|
||||
| `ADD r, v` | Addition, puis résultat modulo 256. |
|
||||
| `SUB r, v` | Soustraction, puis résultat modulo 256 ; `0 − 1` donne 255. |
|
||||
| `MUL r, v` | Multiplication, puis résultat modulo 256. |
|
||||
| `DIV r, v` | Quotient entier non signé ; division par zéro : erreur. |
|
||||
| `MOD r, v` | Reste de la division entière ; diviseur zéro : erreur. |
|
||||
| `AND r, v` | ET bit à bit. |
|
||||
| `OR r, v` | OU bit à bit. |
|
||||
| `XOR r, v` | OU exclusif bit à bit. |
|
||||
| `NOT r` | Inverse les huit bits : `255 − r`. |
|
||||
| `SHL r, v` | Décale vers la gauche, insère des zéros et ne conserve que huit bits. Décalage de 8 ou plus : résultat zéro. |
|
||||
| `SHR r, v` | Décale vers la droite en insérant des zéros. Décalage de 8 ou plus : résultat zéro. |
|
||||
|
||||
Ces calculs ne modifient pas `ZF` ou `CF`. Un dépassement arithmétique ne provoque
|
||||
pas d’erreur ; il est volontairement modulo 256. La division par zéro échoue
|
||||
avant toute modification du registre destination.
|
||||
|
||||
### Comparaison et déroulement
|
||||
|
||||
| Instruction | Effet |
|
||||
| --- | --- |
|
||||
| `CMP x, y` | Compare sans modifier les opérandes : `ZF = (x == y)` et `CF = (x < y)`, en non signé. |
|
||||
| `JMP label` | Saute sans condition. |
|
||||
| `JZ label` | Saute si la dernière comparaison était égale. |
|
||||
| `JNZ label` | Saute si elle était différente. |
|
||||
| `JC label` | Saute si son opérande gauche était strictement inférieur au droit. |
|
||||
| `JNC label` | Saute si son opérande gauche était supérieur ou égal au droit. |
|
||||
| `CALL label` | Empile l’adresse de l’instruction suivante puis saute. Une pile pleine provoque une erreur avant le saut. |
|
||||
| `RET` | Dépile une adresse et y revient. Une pile vide provoque une erreur. |
|
||||
| `WAIT t` | Avance `PC`, termine la tranche d’exécution et attend le nombre de ticks défini ci-dessous. |
|
||||
| `HALT` | Termine le programme, met toutes les sorties redstone à zéro et conserve la RAM. |
|
||||
| `NOP` | Ne fait rien, mais consomme une instruction. |
|
||||
|
||||
Ici `CF` est le résultat « inférieur » de `CMP`, pas une retenue implicite de
|
||||
`ADD`. Les branches lisent exclusivement la dernière comparaison, même après
|
||||
un calcul ou un accès périphérique. `ZF` et `CF` sont faux à l’initialisation.
|
||||
Le registre de données `C` reste distinct de `CF`. Le terminal emploie les noms
|
||||
`ZF` et `CF` ; le programme teste ces indicateurs avec les sauts conditionnels,
|
||||
sans nouvel opérande lisible par `MOV`.
|
||||
Atteindre la fin du programme sans saut a le même effet que `HALT`.
|
||||
|
||||
### Redstone ordinaire
|
||||
|
||||
| Instruction | Effet |
|
||||
| --- | --- |
|
||||
| `IN r, face` | Copie l’intensité entrante, de 0 à 15, observée sur cette face. |
|
||||
| `OUT face, v` | Prépare l’intensité sortante ; la valeur est explicitement bornée à 0–15, donc 42 donne 15. |
|
||||
|
||||
Chaque face possède exactement un mode configuré : **OFF**, **IN**, **OUT** ou
|
||||
**DEVICE**. Les faces sont OFF par défaut. `IN` exige le mode IN et `OUT` le mode
|
||||
OUT ; un mode incorrect provoque une erreur de VM `BAD_PORT_MODE` avant tout
|
||||
effet. OFF et DEVICE n’émettent pas de redstone. Le réglage s’effectue machine
|
||||
arrêtée ou en pause, selon le [contrat des composants](redstone-language-composants.md#contrôleur--la-machine-qui-continue).
|
||||
|
||||
`IN` ne lit ni le registre de sortie du contrôleur ni le contenu exact d’un
|
||||
coffre. Une boucle redstone réellement construite peut revenir sur une entrée ;
|
||||
elle suit alors les mises à jour physiques, sans retour implicite du logiciel.
|
||||
|
||||
## Ticks, entrées et sorties
|
||||
|
||||
La **tranche d’exécution** est le passage d’un contrôleur pendant un tick,
|
||||
jusqu’à `WAIT`, `HALT`, une erreur ou l’épuisement des 64 instructions proposées.
|
||||
Les entrées redstone des contrôleurs sont photographiées dans une phase commune
|
||||
avant leur exécution ; toutes les lectures `IN` d’un même tick voient ces valeurs.
|
||||
Une sortie calculée ce tick ne devient donc pas une nouvelle entrée logicielle
|
||||
d’un autre contrôleur au milieu de sa tranche.
|
||||
|
||||
Les `OUT` alimentent un tampon : la dernière écriture de chaque face l’emporte.
|
||||
Les sorties sont publiées en fin de tranche ; une face sans nouvelle écriture
|
||||
conserve son intensité précédente. `HALT`, erreur et pause de diagnostic imposent
|
||||
zéro et abandonnent les écritures de sortie en attente de cette tranche.
|
||||
Deux `OUT` successifs, 15 puis 0 sans attente entre eux, ne produisent pas une
|
||||
impulsion physique. Il faut laisser au moins un tick entre les deux états.
|
||||
|
||||
L’épuisement du budget conserve `PC`, registres, indicateurs, pile et RAM ; le
|
||||
programme continue à l’instruction suivante lors du prochain tick. Il ne repart
|
||||
jamais silencieusement à son début. Une boucle infinie reste bornée par ce budget.
|
||||
|
||||
`WAIT 1` exécuté au tick `n` reprend au tick chargé suivant ; `WAIT 5` reprend
|
||||
après cinq ticks chargés. L’attente ne consomme pas d’instructions pendant les
|
||||
ticks intermédiaires. Elle mesure des ticks serveur, **pas l’heure civile** ;
|
||||
ralentissement, arrêt du serveur et déchargement ne sont pas du temps simulé
|
||||
en rattrapage. Un futur périphérique d’horloge fournit séparément le temps réel.
|
||||
|
||||
Cette proposition ne charge aucun chunk et n’ajoute aucune énergie obligatoire.
|
||||
Un éventuel coût d’alimentation et le budget global de nombreux contrôleurs
|
||||
restent à éprouver sans confondre alimentation redstone et temps de calcul.
|
||||
|
||||
## Extension par registres périphériques
|
||||
|
||||
Le noyau utilise deux opérations génériques supplémentaires ; les profils des
|
||||
appareils définissent ensuite la signification de leurs registres. Un appareil
|
||||
doit être **physiquement adjacent sur la face choisie, en mode DEVICE**, et
|
||||
exposer ce protocole. `PREAD` ou `PWRITE` sur une face d’un autre mode provoque
|
||||
`BAD_PORT_MODE` avant l’opération ; aucun signal redstone n’est émis en DEVICE.
|
||||
Un coffre vanilla n’acquiert pas spontanément cette interface.
|
||||
Les [profils d’extension](redstone-language-extensions.md) définissent les
|
||||
registres et les commandes de chaque type d’appareil ; ils complètent ce noyau.
|
||||
|
||||
| Instruction | Effet |
|
||||
| --- | --- |
|
||||
| `PREAD r, face, a` | Lit un octet du registre périphérique indiqué. En cas de refus périphérique, conserve `r` et renseigne `STATUS`. |
|
||||
| `PWRITE face, a, v` | Écrit un octet dans ce registre périphérique et renseigne `STATUS`. |
|
||||
|
||||
`STATUS` est lisible par `MOV`, `CMP` et les autres opérandes de valeur. Il ne
|
||||
peut pas être destination. Seules `PREAD` et `PWRITE` exécutées sur une face
|
||||
DEVICE le modifient pendant le programme ; `IN`, `OUT`, les calculs et les
|
||||
sauts le conservent. `BAD_PORT_MODE` laisse aussi sa valeur précédente intacte.
|
||||
|
||||
| Valeur de `STATUS` | Résultat proposé |
|
||||
| --- | --- |
|
||||
| 0 — `OK` | Lecture réussie ou écriture acceptée. |
|
||||
| 1 — `NO_DEVICE` | Aucun périphérique compatible sur cette face. |
|
||||
| 2 — `BAD_ADDRESS` | Registre non exposé par ce profil. |
|
||||
| 3 — `READ_ONLY` | Écriture refusée sur un registre en lecture seule. |
|
||||
| 4 — `WRITE_ONLY` | Lecture refusée sur un registre en écriture seule. |
|
||||
| 5 — `BUSY` | Opération non acceptée parce que l’appareil est occupé ; réessai ultérieur possible. |
|
||||
| 6 — `BAD_VALUE` | Octet valide pour le langage, mais valeur refusée par ce registre. |
|
||||
| 7 — `DEVICE_ERROR` | Appareil en erreur ; consulter son état selon son profil. |
|
||||
|
||||
Les noms de statut sont des libellés de documentation, pas des constantes
|
||||
supplémentaires de l’assembleur V0.1 : le code compare, par exemple, `STATUS`
|
||||
à `0`. Un refus périphérique ne met pas à lui seul la VM en erreur ; le
|
||||
programme peut choisir une attente, une autre branche ou `HALT`.
|
||||
Pour une écriture ayant reçu une réponse, les statuts 1 à 7 signifient que cette
|
||||
écriture n’a pas été acceptée ; une erreur ultérieure d’une tâche acceptée est
|
||||
rapportée par l’état d’opération de l’appareil, pas en réécrivant ce résultat.
|
||||
|
||||
Une écriture `OK` n’est pas une preuve qu’une tâche longue est terminée. Les
|
||||
appareils exposent séparément l’état de leurs opérations, les résultats et leur
|
||||
protocole de confirmation. Une commande déjà acceptée ne doit pas être envoyée
|
||||
de nouveau comme une demande neuve à chaque tick. `BUSY` signifie ici qu’aucune
|
||||
acceptation n’a eu lieu pour cette écriture.
|
||||
|
||||
Contrairement à `OUT`, une écriture périphérique acceptée prend effet lors de
|
||||
l’instruction ; elle ne fait pas partie du tampon redstone. Une erreur ultérieure
|
||||
du programme ne l’annule pas. Les registres de préparation et de validation
|
||||
d’une commande composée doivent appartenir au profil de l’appareil.
|
||||
|
||||
Chaque lecture ou écriture doit être courte et bornée. Un calcul long, transfert
|
||||
matériel ou travail de génération se prépare dans l’appareil puis se suit par
|
||||
son état ; il ne bloque pas le thread serveur dans `PREAD` ou `PWRITE`. Le profil
|
||||
définit aussi les instantanés nécessaires à une lecture de plusieurs octets.
|
||||
Deux octets lus à des moments différents ne forment pas automatiquement un
|
||||
compteur cohérent sur 16 bits.
|
||||
|
||||
Les intensités redstone ne transportent pas implicitement ces octets. Lecteur
|
||||
de conteneur, horloge, aiguillage d’objets, afficheur ou installation d’expansion
|
||||
demandent leurs profils et blocs physiques ; le langage ne donne pas une vue
|
||||
globale des inventaires, des joueurs ou du monde. Il n’existe aucune instruction
|
||||
d’exécution de commandes opérateur, de création libre d’items ou de modification
|
||||
arbitraire de chunks. Voir les [connexions proposées](langage-et-machines.md#connexions-aux-fonctions-de-minecraft--proposition).
|
||||
|
||||
## Diagnostic, arrêt et sauvegarde
|
||||
|
||||
Le terminal distingue **prêt, en marche, en attente, en pause, arrêté pour
|
||||
transport (STOPPED), terminé et en erreur**. Il montre la ligne, `PC`, les
|
||||
registres, `ZF/CF`, `STATUS`, la RAM, la
|
||||
pile, l’attente restante et les intensités d’entrée/sortie. Une erreur de syntaxe,
|
||||
étiquette absente, argument invalide ou dépassement de taille empêche le
|
||||
chargement du nouveau programme et laisse l’ancien intact.
|
||||
|
||||
| Situation | Comportement proposé |
|
||||
| --- | --- |
|
||||
| Autre programme valide chargé | Chargement sur machine arrêtée : document remplacé, RAM à zéro, sorties zéro, `PC` au début, registres et pile à zéro, indicateurs faux, `STATUS = 0`, aucune attente. CPU prêt mais arrêté. Une autre révision assemblée compte comme un autre programme. |
|
||||
| Démarrer / relancer le même programme | Commence au début avec le même état initial de CPU ; conserve la RAM. Relancer n’efface pas les effets des exécutions précédentes. |
|
||||
| Pause de diagnostic | Arrête entre deux instructions, met les sorties à zéro, conserve CPU/RAM/pile/attente. Reprendre continue sans restaurer automatiquement les anciennes intensités. |
|
||||
| `HALT` ou fin du programme | État terminé et sorties zéro ; RAM conservée. Il faut relancer pour recommencer, pas attendre le tick suivant. |
|
||||
| Erreur d’exécution | Arrêt et sorties zéro ; ligne fautive et état conservés pour diagnostic. Pas de redémarrage automatique ; correction/rechargement ou relance explicite. |
|
||||
| Réinitialisation de la machine | Action distincte du chargement : remet aussi la RAM à zéro, conserve le programme, revient à l’état prêt. |
|
||||
| Retrait du terminal ou de la disquette | N’arrête pas le contrôleur ; le programme chargé reste dans la machine. La disquette transporte source et programme, pas l’état courant de RAM/CPU. |
|
||||
| Déchargement / arrêt du serveur | Fige la machine et sauvegarde son état ; aucun tick d’exécution ni d’attente n’est rattrapé hors chargement. |
|
||||
| Rechargement du bloc en place | Restaure l’état et les intensités mémorisées. Avant toute réémission, relit les entrées, la configuration des ports et les connexions ; seuls les ports OUT valides peuvent réémettre si la machine était en marche ou en attente. Pause, STOPPED, fin et erreur conservent des sorties zéro. |
|
||||
| Casse et déplacement | Coupe les sorties et transfère une seule fois le programme et l’état complet, dont RAM, registres, `PC`, pile et attente, dans l’objet récupéré marqué STOPPED. La repose reste arrêtée ; une reprise explicite continue cet état, sorties zéro jusqu’aux prochains `OUT`, ou une relance remet le CPU au début en conservant la RAM. |
|
||||
|
||||
Les erreurs de VM comprennent division par zéro, adresse RAM hors limites,
|
||||
attente nulle, pile d’appels pleine ou vide et mode de face incorrect. Une instruction fautive ne
|
||||
modifie pas partiellement ses registres ou sa RAM. Les instructions déjà
|
||||
terminées, dont les écritures périphériques acceptées, ne sont pas annulées.
|
||||
|
||||
Le pas-à-pas du terminal devra distinguer inspection et exécution réelle d’une
|
||||
instruction, notamment `PWRITE`. Son fonctionnement détaillé reste à concevoir ;
|
||||
une prévisualisation de programme ne doit pas être présentée comme un essai
|
||||
réel réussi sur les machines du monde.
|
||||
|
||||
L’état sauvegardé comprend la version du langage, le programme associé,
|
||||
registres, RAM, `PC`, pile et `SP`, indicateurs, `STATUS`, mode, attente restante,
|
||||
configuration des faces et sorties publiées. La capture se fait à une frontière de tranche achevée,
|
||||
après publication des sorties, sans persister une moitié de calcul ou un tampon
|
||||
`OUT` inachevé. Un état inconnu ou incohérent
|
||||
est conservé pour diagnostic et ne s’exécute pas en étant silencieusement réinitialisé.
|
||||
|
||||
Le format, l’écriture durable et la reprise après crash demandent un ticket
|
||||
propre. La sauvegarde de la VM ne garantit pas seule l’unicité d’une transaction
|
||||
d’objet ou d’expansion : les opérations acceptées appartiennent au contrat de
|
||||
reprise de chaque appareil. Casser le contrôleur ou retirer la disquette ne
|
||||
révoque pas automatiquement une opération déjà enregistrée ailleurs.
|
||||
|
||||
## Exemples avec ce contrat
|
||||
|
||||
### Compter les fronts montants d’une entrée
|
||||
|
||||
RAM `0` garde le compteur modulo 256 ; RAM `1` garde l’état haut précédent.
|
||||
Le programme tourne en continu, une lecture par tick. Un bouton maintenu ne
|
||||
compte qu’une fois ; les impulsions entièrement survenues entre deux relevés
|
||||
ne sont pas promises comme détectées. Les deux cases sont initialisées à zéro
|
||||
sur une machine neuve ; relancer conserve le compteur. Configurer NORTH en IN
|
||||
avant de lancer le programme.
|
||||
|
||||
```text
|
||||
boucle:
|
||||
IN A, NORTH
|
||||
CMP A, 0
|
||||
JZ bas
|
||||
LD B, 1
|
||||
CMP B, 0
|
||||
JNZ attente
|
||||
LD C, 0
|
||||
ADD C, 1
|
||||
ST 0, C
|
||||
MOV D, 1
|
||||
ST 1, D
|
||||
JMP attente
|
||||
bas:
|
||||
MOV D, 0
|
||||
ST 1, D
|
||||
attente:
|
||||
WAIT 1
|
||||
JMP boucle
|
||||
```
|
||||
|
||||
### Un comparateur règle un particuleur
|
||||
|
||||
Le comparateur arrive au nord ; la sortie est mène au particuleur, dans lequel
|
||||
un échantillon a été placé. Le programme transmet uniquement le signal 0–15 :
|
||||
il ne reconnaît pas l’objet du coffre et ne choisit pas l’échantillon. Le
|
||||
particuleur reste un appareil Sanctuary à implémenter, pas un bloc vanilla.
|
||||
Configurer NORTH en IN et EAST en OUT.
|
||||
|
||||
```text
|
||||
boucle:
|
||||
IN A, NORTH
|
||||
OUT EAST, A
|
||||
WAIT 1
|
||||
JMP boucle
|
||||
```
|
||||
|
||||
### Des bouffées de particules avec `WAIT`
|
||||
|
||||
Sur le même particuleur, cinq ticks chargés d’émission puis quinze d’arrêt.
|
||||
Ces valeurs illustrent le programme, pas un coût ou une règle du particuleur.
|
||||
Le temps réel dépend du rythme du serveur ; le retrait du terminal ne l’arrête pas.
|
||||
EAST est configurée en OUT.
|
||||
|
||||
```text
|
||||
boucle:
|
||||
OUT EAST, 15
|
||||
WAIT 5
|
||||
OUT EAST, 0
|
||||
WAIT 15
|
||||
JMP boucle
|
||||
```
|
||||
|
||||
Le prochain ticket devra valider notamment le budget sans remise à zéro du CPU,
|
||||
les bords arithmétiques, l’adressage au-delà de 255, les attentes, le comptage
|
||||
des fronts, la publication des sorties et les transitions de sauvegarde.
|
||||
Les exemples restent des spécifications sur papier ; aucune VM n’a été exécutée
|
||||
pour ce cahier et aucun monde n’a été modifié.
|
||||
@@ -1,203 +0,0 @@
|
||||
# Redstone — objets, cristaux et métiers
|
||||
|
||||
**Exploration WG-26 ; aucun nouveau bloc ou comportement livré.** L’auteur
|
||||
demande de poursuivre la recherche sans se brider : sa réserve visait le
|
||||
porte-outil, pas l’ensemble des phénomènes proposés. Les idées de ce cahier
|
||||
restent à discuter ; aucune n’est validée pour implémentation. L’émeraude paie
|
||||
un villageois pour une tâche de son métier dans son périmètre d’action ; ce
|
||||
rôle économique est retenu indépendamment de la recherche sur les cristaux.
|
||||
|
||||
Le [condensateur et le convoyeur](redstone-language-signaux-et-transport.md)
|
||||
gardent leurs décisions acquises. Transistor, atténuateur et impulseur sont
|
||||
retirés de la sélection. Faisceau à gemmes et suspension d’objets restent des
|
||||
pistes exploratoires ; leur mise de côté venait d’une interprétation trop
|
||||
large du retour de l’auteur. Continuer à chercher ne les transforme pas en
|
||||
fonctionnalités retenues.
|
||||
|
||||
## Villageois — un métier, un lieu, un travail
|
||||
|
||||
**Retenu :** les émeraudes servent à rémunérer des actions cohérentes avec le
|
||||
métier vanilla du villageois, dans un périmètre d’action. Récolte, transformation
|
||||
et services restent les trois secteurs de la [vision](vision.md#villageois-et-automatisation).
|
||||
L’appartenance des villages aux factions via bannière et cloche reste un système
|
||||
à concevoir séparément, notamment pour ses rayons et conflits d’affectation.
|
||||
|
||||
**Contrat de travail proposé :**
|
||||
|
||||
- Le joueur choisit une tâche du métier et lui associe une parcelle ou un atelier,
|
||||
un poste et des conteneurs. La forme exacte de cette attribution reste ouverte ;
|
||||
aucun nouveau bloc « employeur » n’est imposé par cette note.
|
||||
- Le périmètre est identifiable en jeu. Le villageois doit pouvoir atteindre
|
||||
le lieu, disposer des ingrédients ou outils requis et de place pour le résultat.
|
||||
Aucun rayon chiffré n’est arrêté ici.
|
||||
- Le travail est observable : déplacement, manipulation et activité au poste.
|
||||
Les matières sont réellement prises, utilisées et déposées. Un paiement ne
|
||||
génère pas à lui seul un produit à distance.
|
||||
- Un prix en émeraudes correspond à un lot ou service explicite. Proposition :
|
||||
réserver le paiement puis régler le résultat terminé. Une attente devant un
|
||||
coffre plein n’est pas une tâche accomplie.
|
||||
- Chemin coupé, ressources manquantes, destination pleine ou villageois indisponible
|
||||
interrompent le travail. Reprendre ne doit reproduire ni un produit déjà livré
|
||||
ni son paiement. Les règles de réservation et sauvegarde demandent leur ticket.
|
||||
|
||||
| Exemple de tâche Sanctuary proposée | Travail et limite |
|
||||
| --- | --- |
|
||||
| Fermier : entretenir une parcelle de blé | Récolter les plants mûrs, replanter avec les graines disponibles, puis porter la récolte au conteneur associé. Définir si le contrat paie la livraison ou l’entretien complet ; ne pas compter deux fois le même travail. |
|
||||
| Maçon : préparer une commande | Prendre la pierre fournie, travailler à son tailleur de pierre suivant une recette admise et déposer les éléments produits dans le coffre de l’atelier. La commande ne transforme pas les émeraudes en matériaux absents. |
|
||||
|
||||
Ces services payés sont **des comportements Sanctuary à développer**, pas une
|
||||
affirmation qu’un villageois vanilla sait déjà exécuter ces commandes. Le choix
|
||||
des recettes et gestes doit rester cohérent avec le poste et la version du jeu.
|
||||
Les tâches culinaires éventuelles devront respecter les recettes et appareils
|
||||
d’It's Alive ! ; elles ne sont pas déduites d’une profession seule.
|
||||
|
||||
## Porte-outil — éprouver un seul geste d’abord
|
||||
|
||||
**Piste : une tête mécanique reçoit un outil visible.** Une impulsion lui fait
|
||||
accomplir un geste déterminé sur la case immédiatement devant elle. L’outil
|
||||
choisit l’opération ; le circuit détermine quand elle doit se produire.
|
||||
|
||||
Le premier essai à proposer serait **la hache qui écorce une bûche**. Le joueur
|
||||
construit l’amenée et l’évacuation des blocs, par exemple avec des pistons. Le
|
||||
résultat reste une bûche écorcée posée : écorcer ne la casse pas, ne la convertit
|
||||
pas en drop et ne l’envoie pas automatiquement sur un convoyeur.
|
||||
|
||||
Ce geste donne une question vérifiable avant d’agrandir le catalogue : est-ce
|
||||
agréable de construire une petite scierie autour d’une tête à hache ? Une houe
|
||||
qui travaille une terre admissible peut constituer un autre essai, sans valider
|
||||
d’office toutes les utilisations d’outils ou interactions de joueur.
|
||||
|
||||
| Règle proposée | Effet sur la construction |
|
||||
| --- | --- |
|
||||
| Une case d’outil, une tête orientée, une cible voisine | L’objet donne un geste local ; la machine ne parcourt pas une parcelle. |
|
||||
| Une nouvelle impulsion demande une opération | Le signal maintenu ne devient pas une vitesse de travail infinie. Le délai du geste reste à éprouver. |
|
||||
| Cible et conditions admissibles, outil utilisable | Une demande impossible n’effectue rien. L’outil subit l’usure prévue pour un geste réussi. |
|
||||
| Matière réellement présente | Pas de duplication, de produit gratuit ou de transformation d’un objet depuis son simple nom. |
|
||||
| Retrait de l’outil | La tête ne sait plus effectuer le geste ; aucun outil virtuel ne le remplace. |
|
||||
|
||||
La pioche automatique, le combat et les enchantements ne sont pas acquis avec
|
||||
ce bloc. Leur ajout pourrait modifier le rôle des compétences, l’XP ou la
|
||||
progression des joueurs ; il demanderait une décision propre. La fiche ne
|
||||
promet pas une imitation de tous les clics droits ou une identité de joueur
|
||||
simulée dotée de ses permissions.
|
||||
|
||||
Le [distributeur vanilla](audit-redstone-26.3.md#ce-que-la-redstone-permet-de-construire-et-dactionner)
|
||||
et les autres appareils couvrent déjà certaines utilisations d’objets. Avant
|
||||
chaque geste ajouté, vérifier l’apport sur la version cible. Le porte-outil
|
||||
mérite sa place seulement si sa construction apporte un usage apprécié.
|
||||
|
||||
## Articuler acteurs et machines
|
||||
|
||||
Le **villageois** réalise une tâche de métier : il se déplace et organise une
|
||||
suite de manipulations dans son espace. Le **porte-outil** proposé accomplit
|
||||
un geste local répété ; c’est la construction du joueur qui doit lui présenter
|
||||
les bons blocs. Les deux ne doivent pas devenir deux versions du même service.
|
||||
|
||||
Le convoyeur transporte les drops. Le crafter assure les fabrications admises.
|
||||
Le contrôleur coordonne les signaux et interfaces effectivement construites.
|
||||
Il ne donne pas un métier supplémentaire à un villageois et ne transforme pas
|
||||
la monnaie en pouvoir de machine.
|
||||
|
||||
## Phénomènes à explorer
|
||||
|
||||
Ces appareils sont des propositions de discussion, pas un nouveau lot de blocs
|
||||
à coder. L’objet inséré peut définir un geste, une matière ou un accord. Le
|
||||
signal peut ensuite déclencher, maintenir ou mesurer le phénomène. Chaque
|
||||
appareil garde une fonction compréhensible ; une gemme ne constitue pas un
|
||||
prétexte pour réunir des pouvoirs sans rapport dans le même bloc.
|
||||
|
||||
### Siphon thermique
|
||||
|
||||
Un appareil à deux extrémités retire de la chaleur à l’une pour la concentrer
|
||||
à l’autre. Un bâton de Blaze serait une piste de cœur inséré ; cristaux et
|
||||
matériaux de fabrication restent à choisir. Le résultat doit être visible aux
|
||||
deux endroits : refroidissement d’un côté, chauffage de l’autre.
|
||||
|
||||
**Constructions possibles :** une réserve de glace liée à une cuisine ; une
|
||||
serre dont la chaleur vient d’un autre lieu de l’installation ; une porte de
|
||||
glace qui fond quand on redirige le côté chaud. Le monde ne reçoit pas d’office
|
||||
une simulation thermique générale : les transformations admissibles et leur
|
||||
contrepartie matérielle sont à concevoir avant d’annoncer cet appareil.
|
||||
|
||||
### Miroir de matière
|
||||
|
||||
Deux cadres liés échangent le contenu matériel de leur ouverture. Une perle
|
||||
de l’Ender insérée est une piste pour leur liaison ; le mécanisme évoque les
|
||||
opérations spatiales de Sanctuary. Les blocs restent des blocs existants :
|
||||
ils quittent une ouverture pour occuper l’autre, sans copie.
|
||||
|
||||
**Constructions possibles :** un mur se range ailleurs pour ouvrir un passage ;
|
||||
une section d’usine terminée échange sa place avec la prochaine section à
|
||||
travailler. Dimensions, distance, contenu admis et coûts sont ouverts. Les
|
||||
inventaires et machines à état demanderaient un vrai contrat de déplacement,
|
||||
pas une copie de leur apparence. Le lien éventuel aux programmes Galactium
|
||||
est une piste, sans attribuer à ces cadres une charge d’expansion gratuite.
|
||||
|
||||
### Lentille de suspension
|
||||
|
||||
Un cristal maintient un objet libre dans un petit volume visible ; couper le
|
||||
signal le relâche. L’améthyste est une première piste visuelle pour le cœur,
|
||||
pas une correspondance retenue.
|
||||
|
||||
**Constructions possibles :** attendre avec une récolte suspendue au-dessus
|
||||
d’un convoyeur ; exposer un butin dont la chute dépend d’un circuit. Une
|
||||
extension aux projectiles permettrait une épreuve de salves ; elle doit être
|
||||
évaluée séparément. Quantité retenue, mouvement, âge et ramassage doivent être
|
||||
distingués : suspendre ne signifie pas créer une copie dans un inventaire caché.
|
||||
|
||||
### Prisme accordé
|
||||
|
||||
Un cristal taillé dans un émetteur définit l’accord d’un faisceau visible ;
|
||||
un récepteur portant le même accord restitue le signal reçu. Les différentes
|
||||
gemmes pourraient distinguer les accords sans devenir cinq niveaux de puissance.
|
||||
|
||||
**Constructions possibles :** commander un mécanisme visible sur une autre
|
||||
plateforme ; ouvrir une porte en alignant un trajet entre des ouvertures.
|
||||
Portée, obstacles, visibilité et motifs restent à essayer. Une lentille en rubis
|
||||
serait un composant matériel, pas un prélèvement de monnaie à chaque impulsion.
|
||||
Les données complexes gardent les interfaces prévues dans les autres cahiers.
|
||||
|
||||
### Mémoire de geste
|
||||
|
||||
Une variante du porte-outil pourrait recevoir un support de mémoire et un
|
||||
outil : le joueur montre un geste, l’appareil en conserve une description puis
|
||||
le reproduit sur la cible présentée. Le lapis serait une piste de matériau
|
||||
d’inscription, sans décision fixée.
|
||||
|
||||
**Constructions possibles :** enseigner l’écorçage d’une bûche, puis construire
|
||||
une amenée par pistons ; enregistrer un geste de culture dans un atelier.
|
||||
Cela pourrait aussi se révéler redondant avec les programmes sur disquette :
|
||||
son intérêt serait l’apprentissage par démonstration et la présence de l’outil,
|
||||
pas un deuxième langage imposé ou un joueur artificiel capable de tout faire.
|
||||
|
||||
### Diffuseur d’appât et cœur de serre
|
||||
|
||||
Deux autres pistes donnent une fonction à des objets ordinaires :
|
||||
|
||||
- **Diffuseur d’appât :** la nourriture insérée attire les animaux intéressés
|
||||
dans un voisinage visible. On peut déplacer des points d’attraction pour
|
||||
guider un troupeau dans un sas. Attirer, nourrir et reproduire sont des
|
||||
opérations distinctes ; aucune n’est implicitement accordée par les autres.
|
||||
- **Cœur de serre :** la matière introduite contribue à refroidir, chauffer
|
||||
ou humidifier une installation construite avec parois et ouvertures. On
|
||||
rapporte d’abord une plante rare d’un continent, puis on construit chez soi
|
||||
les conditions permettant sa culture. L’appareil ne fournit pas la graine
|
||||
inconnue. Le siphon thermique pourrait en devenir un composant, au lieu de
|
||||
créer deux systèmes de chaleur indépendants.
|
||||
|
||||
Ces mécanismes restent ouverts à la critique. Un premier essai devrait montrer
|
||||
le geste et deux constructions possibles, avant de fixer recettes ou budgets.
|
||||
|
||||
## Cristaux et gemmes — préserver leurs rôles et explorer leurs matières
|
||||
|
||||
Améthyste et lapis restent des matériaux à explorer ; les correspondances
|
||||
techniques ci-dessus sont des propositions, aucune n’est retenue. Émeraudes pour les
|
||||
villageois, rubis pour les échanges entre joueurs et shops, saphirs pour les
|
||||
achats et réservations prévus conservent leur rôle économique.
|
||||
|
||||
Le principe « un objet donne sa fonction à un appareil » reste une piste, déjà
|
||||
illustrée par le particuleur. L’émeraude peut rémunérer un acteur tandis qu’une
|
||||
autre pierre participe matériellement à un appareil ; un rôle n’impose pas
|
||||
un autre rôle. L’exploration n’exige pas de remplir une table de cinq pouvoirs
|
||||
correspondant aux cinq gemmes mentionnées. Elle reste ouverte, sans nouveaux
|
||||
blocs ni coûts ajoutés au jeu.
|
||||
@@ -1,266 +0,0 @@
|
||||
# Redstone Language — signaux et transport visible
|
||||
|
||||
**Conception WG-26 ; aucun nouveau bloc livré.** Deux appareils
|
||||
autonomes sont retenus pour le premier essai : un **condensateur de signal** et un
|
||||
**convoyeur en cuir**. Ils complètent les
|
||||
[composants informatiques](redstone-language-composants.md), les
|
||||
[instructions proposées](redstone-language-instructions.md) et les
|
||||
[extensions et périphériques](redstone-language-extensions.md).
|
||||
|
||||
Le condensateur, ses faces, son maintien et ses gestes sont validés en conception.
|
||||
Le convoyeur transporte des drops sur une bande posée comme un rail au sol,
|
||||
avec une seule vitesse. **Il reste arrêté à la pose sans alimentation**, afin
|
||||
de pouvoir éteindre une usine. Sa recette en H est retenue. La commande de
|
||||
marche et le choix du sens proposés sont distingués ci-dessous.
|
||||
**Les valeurs d’essai et les comportements encore proposés sont
|
||||
signalés ci-dessous** : ces décisions de conception ne sont pas une livraison.
|
||||
Le contrôleur peut piloter les appareils par la redstone ordinaire ; ils ne
|
||||
demandent ni terminal ni nouveau réseau de données pour fonctionner.
|
||||
|
||||
## Condensateur — faire évoluer une intensité
|
||||
|
||||
Le condensateur conserve une **valeur de signal de 0 à 15**, qui rejoint
|
||||
progressivement la valeur d’entrée. Il n’accumule pas de RF, de combustible
|
||||
ou d’énergie capable d’accélérer une machine. Son intérêt est un changement
|
||||
graduel et visible : montée d’une jauge, apparition d’un effet, extinction
|
||||
progressive ou commande qui amortit des variations brèves.
|
||||
|
||||
### Faces et gestes retenus
|
||||
|
||||
| Élément | Contrat retenu, sauf valeur explicitement proposée |
|
||||
| --- | --- |
|
||||
| Pose | Bloc plein, orientation horizontale ; flèche vers la sortie. Entrée arrière, sortie avant. Le corps présente une réglette de charge. |
|
||||
| Entrée arrière | Lire uniquement l’intensité réellement reçue sur cette face, notée `S`, de 0 à 15. Une alimentation latérale ne remplace pas cette entrée. |
|
||||
| Sortie avant | Émettre la charge courante `Q` comme signal faible, sur cette seule face ; aucune sortie forte ni gain au-delà de 15. |
|
||||
| Maintien par dessus | Un signal positif sur la face supérieure gèle charge et délai restant. La sortie garde sa valeur : ce maintien n’est pas un arrêt électrique. |
|
||||
| Autres faces | Pas d’entrée supplémentaire et pas de relais omnidirectionnel du signal. Un comparateur peut lire la charge du bloc. |
|
||||
| Clic droit, main vide | Régler le délai `N` entre deux paliers : cycle proposé **1, 2, 4, 8 ticks**, avec **4** par défaut. Le réglage est visible sur le corps. |
|
||||
| Sneak + clic droit, main vide | Décharger immédiatement à zéro et réinitialiser le délai. Si l’entrée reste positive, la charge recommence ensuite normalement. |
|
||||
| Objet tenu | Aucun échantillon, carburant ni inventaire. Sneak avec un bloc garde le geste de construction contre l’appareil. |
|
||||
| Comparateur | Valeur `Q`, de 0 à 15 ; ce n’est pas un nombre d’objets ni une réserve d’énergie. |
|
||||
|
||||
### Profil de charge proposé
|
||||
|
||||
Le principe de charge et de décharge progressives est retenu. Pour le premier
|
||||
profil d’essai, lorsque le maintien est inactif, chaque intervalle de `N` ticks
|
||||
actifs permet **un seul palier** :
|
||||
|
||||
- si `Q < S`, augmenter `Q` de 1, sans dépasser `S` ;
|
||||
- si `Q > S`, diminuer `Q` de 1, sans descendre sous `S` ;
|
||||
- si `Q = S`, garder la valeur ; une nouvelle différence commence un intervalle.
|
||||
|
||||
L’entrée est la cible et le plafond **pendant la charge**. Si elle baisse,
|
||||
la charge peut rester temporairement supérieure pendant sa décharge : passer
|
||||
de 15 à une entrée 5 descend jusqu’à 5 ; couper l’entrée descend jusqu’à 0.
|
||||
Un plafond appliqué instantanément à la baisse supprimerait précisément cet
|
||||
effet de condensateur. Charge et décharge utilisent le même `N` dans ce premier
|
||||
essai ; pas de deuxième réglage avant d’en démontrer l’utilité.
|
||||
|
||||
Les valeurs de `N`, le pas de charge et la durée résultante restent des réglages
|
||||
d’essai. Exemple proposé avec `N = 4` : une entrée stable à 15 fait passer la
|
||||
sortie de 0 à 15 en 60 ticks actifs ; la coupure la ramène à zéro en 60 ticks. À 20 ticks par
|
||||
seconde, chaque trajet dure trois secondes. Ce délai suit les ticks effectivement
|
||||
exécutés, sans promettre trois secondes réelles lorsque le serveur ralentit.
|
||||
Changer `N` garde `Q` et commence un nouvel intervalle ; cela ne crée aucun
|
||||
palier supplémentaire au clic.
|
||||
|
||||
Le maintien supérieur suspend aussi l’intervalle en cours. À sa levée, on relit
|
||||
l’entrée et on poursuit le délai restant vers cette cible actuelle. La réglette
|
||||
indique la charge ; un second indice de forme ou d’animation signale le maintien.
|
||||
Un signal en entrée ne fait jamais effectuer plusieurs ticks de machine à la fois.
|
||||
|
||||
### Conservation, retrait et bord de chunk
|
||||
|
||||
**Proposition :** charge, réglage et délai restant sont sauvegardés dans le
|
||||
bloc posé. Un chunk déchargé n’avance plus ; au retour, les faces et l’entrée
|
||||
sont relues avant de remettre la sortie en service. Aucune charge ou décharge
|
||||
hors ligne n’est rattrapée. L’appareil ne charge pas le chunk voisin pour
|
||||
demander son signal : une connexion indisponible reprend quand le voisin est
|
||||
réellement actif, sans calcul de trajet à distance.
|
||||
|
||||
Le bloc ne se déplace pas par piston. Sa casse coupe sa sortie ; la récupération
|
||||
rend un seul appareil avec son réglage `N`, mais **sans charge conservée**.
|
||||
Cette remise à zéro est propre à ce composant, distincte de la mémoire portable
|
||||
proposée pour le contrôleur. Une destruction sans butin ne fabrique aucun objet
|
||||
de charge. Pas de waterlogging ni de simulation de court-circuit dans ce premier
|
||||
essai ; le boîtier plein suit le support d’un bloc ordinaire.
|
||||
|
||||
## Convoyeur en cuir — déplacer les drops
|
||||
|
||||
Le convoyeur fait circuler **les entités d’objets présentes sur sa surface**.
|
||||
Il ne contient pas de slots et ne devient pas un hopper invisible. Un dropper
|
||||
dépose les objets, le tapis les transporte, puis un dispositif réel les reçoit.
|
||||
Une stack au sol reste la même quantité d’objets pendant ce trajet.
|
||||
|
||||
### Forme retenue et commandes proposées
|
||||
|
||||
| Élément | Contrat et statut |
|
||||
| --- | --- |
|
||||
| Matière et forme | **Retenu :** bande en cuir, posée comme un rail au sol, avec un support. Rouleaux et châssis visibles restent une proposition d’apparence. |
|
||||
| Repos | **Retenu :** arrêté sans alimentation, notamment à la pose sur un emplacement non alimenté. Poser une bande ne la fait pas fonctionner toute seule. |
|
||||
| Direction | **Proposé :** une flèche choisit le sens à la pose ; clic droit, main vide, pour inverser cette flèche. Le réglage manuel et son interaction avec les raccords restent à valider. |
|
||||
| Signal redstone | **Retenu :** 0 arrête l’entraînement. **Convention proposée :** de 1 à 15, marche dans le sens de la flèche à la même vitesse. La force du signal ne choisit ni une vitesse ni un sens inverse. |
|
||||
| Sortie électrique | **Proposé :** aucune. Le convoyeur ne relaie pas une alimentation forte et ne fournit pas de comparateur d’inventaire. |
|
||||
| Entrée/sortie d’objets | **Retenu :** des drops tombent ou glissent sur la bande. Ni aspiration d’un coffre voisin, ni insertion directe dans son inventaire. |
|
||||
| Vitesse | **Retenu :** une seule vitesse. **Valeur d’essai : 0,08 bloc par tick actif**, soit 1,6 bloc par seconde à 20 ticks/seconde. Une cible de déplacement local, pas une téléportation ni une augmentation du tick rate. |
|
||||
| Transport concerné | Objets au sol. L’entraînement des joueurs, mobs, bateaux ou wagons n’est pas retenu dans ce contrat. |
|
||||
|
||||
La recette retenue utilise **six bâtons et un cuir central**, disposés en H.
|
||||
Interprétation de la grille : `B` = bâton, `C` = cuir, `.` = case vide.
|
||||
|
||||
```text
|
||||
B.B
|
||||
BCB
|
||||
B.B
|
||||
```
|
||||
|
||||
Le **nombre de convoyeurs produits n’est pas fixé**. L’ancien contrat
|
||||
« 0 = avance, 1–15 = sens inverse » est **remplacé** par la demande d’une usine
|
||||
éteignable : sans alimentation, la bande ne tourne pas. Le réglage du sens
|
||||
reste distinct de la commande de marche ; aucun arrêt manuel mémorisé n’est
|
||||
nécessaire à cette proposition.
|
||||
|
||||
### Inversion automatique — moteur optionnel proposé
|
||||
|
||||
Un **moteur de convoyeur** pourrait apporter deux commandes redstone distinctes :
|
||||
**marche** et **inversion**. Proposition : marche à zéro garde la bande arrêtée,
|
||||
quelle que soit l’inversion ; marche positive autorise la vitesse unique,
|
||||
et l’autre commande choisit le sens nominal ou inverse. Une seule intensité
|
||||
0–15 n’essaie donc pas de coder simultanément arrêt, marche et direction.
|
||||
|
||||
Ce moteur reste **une proposition, pas un bloc retenu ni un équipement
|
||||
obligatoire** : la bande simple garde sa commande ordinaire et son réglage
|
||||
manuel proposé. Ses faces, son raccord physique aux bandes, sa portée bornée,
|
||||
le traitement des embranchements et la priorité entre plusieurs moteurs
|
||||
restent à définir. Aucune propagation infinie le long des convoyeurs ni
|
||||
activation de chunks à distance n’est admise implicitement. La fonction est
|
||||
à raccorder au cahier des [extensions](redstone-language-extensions.md) si
|
||||
elle est retenue ; aucun protocole de liaison n’est encore fixé.
|
||||
|
||||
### Nouvelle piste — vitesse par cadence
|
||||
|
||||
L’auteur demande si le « signal horaire » pourrait modifier la vitesse. La
|
||||
proposition interprète ici ce terme comme **une horloge redstone à impulsions**,
|
||||
pas une heure du calendrier ; cette interprétation reste à confirmer. Ce n’est
|
||||
pas encore un remplacement validé de la marche continue à vitesse unique.
|
||||
|
||||
**Proposition : une impulsion fait avancer d’un pas fixe.** La fréquence des
|
||||
impulsions règle la vitesse moyenne ; leur absence arrête le tapis. Le sens
|
||||
reste réglé séparément. Seul un front montant compte : une alimentation maintenue
|
||||
produit un pas, pas une succession de pas. Le retour à zéro prépare l’impulsion
|
||||
suivante. L’intensité 1–15 ne modifie pas la distance d’un pas dans cette variante.
|
||||
|
||||
Le pas et la fréquence maximale restent à éprouver. Le mouvement peut être
|
||||
animé entre deux positions, sans déplacement instantané à travers une collision.
|
||||
Si un pas est déjà en cours ou bloqué, une nouvelle impulsion ne doit pas créer
|
||||
une réserve illimitée de mouvements à rejouer ensuite. Le choix entre refus
|
||||
visible et courte attente devra appartenir à la fiche du moteur retenu.
|
||||
|
||||
**Montage possible :** faire avancer une pièce, attendre l’opération d’un poste,
|
||||
puis envoyer l’impulsion suivante. Un contrôleur pourrait varier la cadence,
|
||||
mais une horloge construite en redstone suffirait à la marche régulière.
|
||||
|
||||
Deux comportements restent donc à comparer : **alimenté = marche continue**,
|
||||
ou **impulsion = un pas**. Ils ne doivent pas être mélangés silencieusement sur
|
||||
la même entrée. Un mode explicite ou deux variantes de moteur seraient des
|
||||
solutions à discuter si les deux usages sont souhaités.
|
||||
|
||||
### Raccords et déplacement à essayer
|
||||
|
||||
L’autoconnexion, les virages et les pentes sont des **propositions à essayer**
|
||||
pour retrouver une pose familière de rails. Leur géométrie et leur comportement
|
||||
en inversion ne sont pas encore fixés ; cette fiche ne les présente ni comme
|
||||
implémentés ni comme exclus.
|
||||
|
||||
Un objet soutenu par une bande alimentée suit sa ligne centrale et sa direction,
|
||||
en respectant les collisions. Une correction latérale modeste peut le recentrer
|
||||
sans traverser un obstacle. Le déplacement est attribué au segment sous son
|
||||
centre et appliqué **une seule fois par tick** : chevaucher deux bandes ne
|
||||
double pas la vitesse. La hauteur basse permet de garder l’objet accessible
|
||||
aux interactions ordinaires.
|
||||
|
||||
Deux bandes adjacentes dont les tracés et mouvements se raccordent peuvent se
|
||||
transmettre l’objet. Pour les raccords incompatibles, la résolution reste à
|
||||
essayer avec les virages et l’inversion : **ne jamais dupliquer les drops ni
|
||||
additionner deux entraînements dans le même tick**. Aucun transfert fictif
|
||||
ne traverse une collision. Une convergence ne devient pas un trieur par type.
|
||||
|
||||
Un mur ou une sortie obstruée arrête le mouvement avant collision. Les objets
|
||||
peuvent se rapprocher et fusionner selon les règles normales des stacks ; il
|
||||
n’existe pas de file d’attente d’inventaire cachée ni de pression qui pousse un
|
||||
objet à travers un bloc. Couper l’alimentation supprime l’entraînement de la
|
||||
bande ; cela ne fige pas les entités contre un ramassage, un courant ou une
|
||||
autre force. Une inversion agit sur les drops encore présents ;
|
||||
elle ne récupère pas ceux déjà ramassés, collectés ou tombés hors de la bande.
|
||||
|
||||
### Réception, chute et distance
|
||||
|
||||
Une bande peut déboucher sur une chute, un autre tapis plus bas, un bac ouvert
|
||||
ou une zone de collecte. À un bout libre dans un chunk actif, l’objet quitte la
|
||||
bande puis suit sa physique normale : aucune plateforme invisible ne le sauve
|
||||
du vide. Un coffre fermé en face ne constitue pas un récepteur. Un hopper ou un
|
||||
hopper minecart peut recueillir les drops depuis une position où sa collecte
|
||||
les atteint réellement ; l’aspiration sous le châssis devra être vérifiée sur
|
||||
la forme retenue, sans promettre un passage à travers un bloc plein.
|
||||
|
||||
Le comparateur du coffre de réception mesure ce coffre ; le tapis n’en connaît
|
||||
pas spontanément le stock. Avec une logique de seuil, le circuit peut arrêter
|
||||
**le déclenchement du dropper en amont et couper l’alimentation des bandes**.
|
||||
Les objets restent accessibles sur la ligne. Un retour vers un autre bac
|
||||
pourrait ensuite utiliser le changement manuel de sens ou le moteur optionnel,
|
||||
si cette inversion est retenue. Le bac doit réellement recueillir les drops ;
|
||||
ni le tapis ni le moteur ne connaissent spontanément les capacités de réception.
|
||||
|
||||
La longueur est celle des bandes réellement posées. Il n’y a pas de portée
|
||||
magique entre deux points ni de recherche de route globale. À la frontière
|
||||
d’un chunk non actif, le tapis retient l’objet avant le transfert : pas de
|
||||
chargement forcé, de dépôt dans un inventaire fictif ou de copie différée.
|
||||
Quand les deux côtés sont actifs, le passage reprend depuis l’objet existant.
|
||||
La bande doit aussi être alimentée. Une frontière chargée ne donne aucun
|
||||
déplacement additionnel.
|
||||
|
||||
**Conservation proposée :** les objets gardent leur quantité, composants,
|
||||
dommages, âge et règles normales de ramassage. Le convoyeur ne réinitialise pas
|
||||
leur durée de vie et n’accorde pas une conservation infinie à une longue ligne.
|
||||
Décharger un chunk suspend son travail, sans simuler ensuite les déplacements
|
||||
manqués. Les interactions concurrentes — ramassage, fusion, collecte ou casse —
|
||||
doivent concerner l’entité encore présente, jamais une seconde représentation.
|
||||
|
||||
### Casse et réglages visuels
|
||||
|
||||
Proposition de sauvegarde : le segment conserve son orientation, son sens
|
||||
manuel et sa forme, puis relit l’alimentation pour autoriser la marche au
|
||||
chargement ; il reste arrêté à zéro. Il n’a aucun contenu
|
||||
à extraire lors de sa casse : les objets déjà dessus restent dans le monde et
|
||||
peuvent tomber si leur support disparaît. Perdre le bloc de support dépose la
|
||||
bande une seule fois. Un outil approprié au châssis permet sa récupération ;
|
||||
outil exact et temps de casse restent des réglages de fabrication. La recette
|
||||
en H est retenue, son rendement reste ouvert.
|
||||
|
||||
Proposition initiale : segments non poussables par piston et non waterloggables.
|
||||
En présence d’eau au-dessus, l’entraînement est suspendu et les drops suivent
|
||||
le courant ; la lave garde son danger ordinaire. La flèche nominale reste
|
||||
lisible ; l’animation indique le sens de mouvement actif. Le cuir n’est pas
|
||||
une nouvelle source d’énergie et aucun carburant n’est ajouté par ces fiches.
|
||||
|
||||
## Pistes écartées — archive de décision
|
||||
|
||||
- **Transistor redstone :** écarté par l’utilisateur, retiré de la sélection.
|
||||
- **Atténuateur réglable :** écarté par l’utilisateur, retiré de la sélection.
|
||||
- **Impulseur / accélérateur :** écarté par l’utilisateur, retiré de la sélection.
|
||||
|
||||
## Montages d’essai
|
||||
|
||||
| Construction | Enchaînement observable |
|
||||
| --- | --- |
|
||||
| Fumée d’atelier progressive | Levier → condensateur → particuleur. La fumée monte puis décroît après coupure ; le même échantillon conserve le type d’effet. |
|
||||
| Indicateur de réserve amorti | Comparateur de coffre → condensateur → afficheur analogique. L’écran montre une tendance retardée, explicitement différente du stock instantané ; cette mesure ne sert pas à décider un paiement. |
|
||||
| Ligne de fabrication visible | Dropper → convoyeur en cuir alimenté → chute de collecte → hopper/coffre. Couper les déclenchements du dropper et l’alimentation des bandes éteint la ligne ; les drops restent dans le monde. |
|
||||
| Réception protégée | Comparateur du coffre et logique de seuil → arrêt des déclenchements du dropper en amont et coupure des bandes. Le circuit traite les apports et les drops en trajet, sans retourner automatiquement le tapis à la coupure. |
|
||||
| Retour vers un bac — proposition | Après arrêt des apports, changer le sens manuellement puis réalimenter ; si le moteur optionnel est retenu, séparer sa commande d’inversion de sa commande de marche. Le bac de retour est une construction réelle. |
|
||||
|
||||
Les essais devront distinguer collisions, limites de chunk, signal réellement
|
||||
reçu, maintien du condensateur, arrêt/reprise des bandes, inversion proposée et quantité d’objets
|
||||
avant/après transfert. Ils précèdent les réglages définitifs et l’ajout au pack.
|
||||
Ces appareils conservent leurs usages autonomes ; une installation programmée
|
||||
réemploie les mêmes faces et les mêmes règles, sans les contourner.
|
||||
@@ -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, Efe, 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,510 +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 place des Lost Cities
|
||||
dans les continents d’expansion, la sélection des vestiges, leurs usages et
|
||||
leurs relations. Les futurs ordinateurs et afficheurs influencent les espaces
|
||||
à concevoir. 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 | Sanctuary Island conserve ses secrets, salles cachées et passages souterrains : observatoire, grande salle d’expansion et atelier caché, avec la grande traversée. Retirer les bâtiments ordinaires de la future sélection de génération de l’île. Ajouter le grand laboratoire des Cavernes ; l’accès aux raids se découvre sur une île d’expansion. |
|
||||
| Voies abandonnées | Conserver le train et les rails au milieu de l’île : leurs tracés donnent des directions aux constructions des joueurs et invitent à réparer le réseau. Aucun bâtiment ni gare obligatoire à leurs extrémités. |
|
||||
| Sites flottants initiaux | Envisager un ou deux îlots suspendus riches avec un coffre enterré, des bateaux marchands répartis en périphérie de Sanctuary et des montgolfières contenant des richesses. Les temples quittent la génération de base. Une première structure volante sur ou au-dessus de Sanctuary Island reste souhaitée ; dimensions, distribution et visibilité sont à préciser en conservant le départ naturel. |
|
||||
| Villes et regroupements | Les Lost Cities restent destinées aux expansions ; ce cadrage ne les ramène pas sur Sanctuary Island. Elles peuvent déborder de petites îles de rayon 64 blocs si leurs surplombs, raccords et constructions restent cohérents. Cet assouplissement est ciblé, sans forçage général des placements. |
|
||||
| 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 essentielle et rééquipable, indépendamment d’une ville. Les joueurs peuvent construire une autre installation d’expansion ailleurs ; la salle historique n’est pas un monopole géographique. « Salle de transformation » est rapproché de cette salle comme correspondance de travail, à confirmer. |
|
||||
| Réseau de la salle | Privilégier les égouts et canalisations éclairés par la redstone, avec plusieurs galeries d’accès à la grande salle. La trappe reste un accès parmi les autres. La piscine près de cette salle quitte la sélection active ; les autres pool rooms restent en réserve. |
|
||||
| Rencontres des expansions | Concevoir de grands continents avec océans, temples ou monuments sous-marins pour un futur boss aquatique, ainsi qu’un volcan abritant un mob géant. Identités, implantation, rencontres et récompenses restent à définir. |
|
||||
| Galactium | Nom retenu pour tout le système d’expansion. Casser un noyau le fait disparaître sans objet récupérable, provoque une réaction du serveur en galactique et libère une charge collective. Une interface est demandée ; la vue dans le terminal reste une proposition. |
|
||||
| Langage et machines | Employer l’écriture galactique comme langage secret reliant redstone programmable, expansions, waystones et End. Programmer directement en assembleur des contrôleurs avec six faces d’entrée/sortie et une RAM adressable. Socle retenu : contrôleur, terminal, afficheur et disquettes ; le bloc Assembleur est retiré, l’assemblage du code appartient au terminal. Base proposée : état complet sauvegardé, liaison série supplémentaire en réserve ; détails et apprentissage à définir. |
|
||||
| Découverte des machines | L’exploration doit révéler les premiers usages concrets du terminal, du contrôleur, de l’afficheur programmable et des disquettes, associés à des coffres, hoppers, comparateurs et autres blocs vanilla. Concevoir des scènes spectaculaires dont les systèmes peuvent être compris, reproduits et adaptés par les joueurs. |
|
||||
| Accès et apprentissage | Intégrer des waystones comme ascenseurs dans les îles générées : une entrée naturelle conduit à une installation ou un laboratoire inférieur. Leur usage fait découvrir le transport vertical et la téléportation horizontale précise, coûteuse en XP. Aucun nouveau palier abstrait n’est ajouté pour enseigner cet usage. |
|
||||
| Salles de machines | Concevoir des salles plus grandes, accueillant plusieurs systèmes de farming avec ordinateurs. Apprendre en observant blocs, flux, redstone et programmes consultables, sans panneau explicatif ni texte tutoriel. Les afficheurs fonctionnels restent présents. |
|
||||
| Joueurs | Viser une expérience de RPG Minecraft pour environ vingt joueurs aux pratiques variées : construction, redstone, minage, PvP et PvE. Cette direction ne change pas les dimensions des mondes ni n’instaure des classes obligatoires. |
|
||||
| Donjon majeur | Un immense laboratoire caché dans l’île abrite le portail des Cavernes et son gardien : zombie géant ou golem de pierre, choix ouvert. Vaincre le gardien permet de stabiliser et débloquer les portails collectivement et définitivement ; tous peuvent ensuite construire leur portail. Annoncer cette première réussite comme un achievement de serveur. L’objet de quête évoqué auparavant et son usage restent à raccorder à cette victoire. |
|
||||
| Secret des raids | Un lieu caché sur une île générée par expansion abrite un portail exigeant dix joueurs pour lancer un raid. Un raid par semaine pour tout le serveur ; donjon procédural difficile en aventure et butin matériel commun réparti par les joueurs. |
|
||||
| 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.
|
||||
|
||||
Dans la dernière synthèse de l’auteur, **« grande salle de transformation »** est
|
||||
comprise provisoirement comme la grande salle d’expansion déjà prévue. Cette
|
||||
correspondance sert à poursuivre sa conception : elle n’ajoute pas une seconde
|
||||
salle, ne renomme aucun identifiant et ne lui attribue pas une nouvelle fonction
|
||||
de transformation matérielle. Son rôle essentiel demande de prévoir un accès et
|
||||
un emplacement adaptés sans dépendre de la présence d’une Lost City.
|
||||
|
||||
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).
|
||||
|
||||
## Préparation de l’alpha.23 — sans démarrer l’implémentation
|
||||
|
||||
L’auteur souhaite reprendre le code **après cette discussion**, pas pendant
|
||||
ce cadrage. La prochaine passe doit dépeupler la surface des bâtiments qui ne
|
||||
font pas partie des secrets retenus, tout en préservant le train, les rails,
|
||||
la grande traversée et les accès souterrains. Retirer une gare ou une scierie
|
||||
du catalogue actif de l’île ne doit pas supprimer par dépendance son tracé
|
||||
ferroviaire. Un arrêt dans la nature ou une voie interrompue peuvent constituer
|
||||
une invitation à construire, sans bâtiment à générer pour les justifier.
|
||||
|
||||
**Proposition de découpage pour l’alpha.23 :** vérifier d’abord la sélection,
|
||||
les réseaux et les volumes du laboratoire des Cavernes sur l’île initiale.
|
||||
Réserver une rencontre de grande taille et un espace pour son portail ;
|
||||
concevoir séparément l’accès aux raids comme découverte d’expansion. Les mobs,
|
||||
portails fonctionnels, achievements collectifs et instances hebdomadaires
|
||||
demandent leurs propres contrats avant de promettre leur livraison dans la 23.
|
||||
Les structures écartées restent réutilisables pour les expansions ou une autre
|
||||
sélection. Aucun bâtiment n’est effacé des mondes existants par ce travail.
|
||||
|
||||
## 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.
|
||||
|
||||
Les ensembles urbains trouvent désormais surtout leur place sur les continents
|
||||
d’expansion. Le catalogue de leurs bâtiments reste disponible pour cette future
|
||||
conception ; cela ne garantit pas une ville sur chaque continent. Sur Sanctuary
|
||||
Island, les découvertes isolées et souterraines laissent la surface naturelle
|
||||
disponible pour les nouvelles constructions. L’observatoire conserve son besoin
|
||||
d’un ciel dégagé et doit trouver sa place dans ce relief, à découvrir en explorant.
|
||||
|
||||
Cette nouvelle répartition est une direction de conception : l’alpha.22 publiée
|
||||
conserve ses règles et ses structures. Les bâtiments en réserve ne sont ni
|
||||
supprimés des sauvegardes existantes ni déplacés vers un autre continent par ce
|
||||
document.
|
||||
|
||||
## 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 et les voies ferrées abandonnées. Le donjon majeur
|
||||
prend la forme d’un immense laboratoire caché. Le secret du portail des raids
|
||||
est désormais réservé à une île d’expansion. Les plans, emplacements et fréquences
|
||||
restent à définir,
|
||||
avec une présence à assurer pour les lieux nécessaires à la progression.
|
||||
La piscine près de la salle d’expansion est retirée de la sélection
|
||||
active. Les autres piscines ou pool rooms et l’excavation restent 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 | Grand volume souterrain essentiel ayant accueilli une installation collective ; plusieurs accès par des galeries d’égouts et de canalisations, dont une trappe, même sans ville au-dessus. | Découverte majeure et espace rééquipable, avec éclairage redstone et raccordements lisibles. La piscine voisine est retirée ; état initial et éléments récupérables à choisir. | Assembler une installation d’expansion ici ou ailleurs. « Salle de transformation » désigne provisoirement ce même lieu ; aucune fonction supplémentaire n’en est déduite. |
|
||||
| Atelier caché | Entrée discrète intégrée au terrain, pouvant ouvrir sur de grandes salles d’installation ou de laboratoire ; plans et dimensions à dessiner. | Outillage et plusieurs systèmes de farming à comprendre et essayer ; traces d’un travail interrompu. Contenu exact à choisir. | Installations reproductibles, programmes partageables, duplicables et revendables. Leur présence jouable dépendra de leur implémentation ; aucune machine nouvelle n’est ajoutée aux coffres de l’alpha publiée. |
|
||||
| Laboratoire caché — donjon majeur | Immense installation souterraine abritant le portail des Cavernes, distincte de la salle d’expansion. L’ancien usage exact reste à concevoir. | Exploration difficile et gardien à vaincre : zombie géant ou golem de pierre. Prévoir les volumes de combat et les circulations avant de fixer le mob. | Stabilisation collective et permanente des portails du monde de minage, annoncée à tout le serveur. Objet de quête et geste d’activation à préciser. |
|
||||
| Accès secret aux raids — expansion | Lieu caché sur une île générée par expansion, avec un portail et une aire de rassemblement pour dix joueurs. Architecture distincte du laboratoire à dessiner. | Découvrir le lieu et s’y réunir ; le donjon du raid se déroule dans une instance, pas dans la traversée ordinaire. | Un raid hebdomadaire pour le serveur, aventure procédurale difficile, butin commun matériel à partager librement. |
|
||||
| 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. |
|
||||
| Autres piscines ou pool rooms — en réserve | Installation hydraulique avec un premier usage identifiable, puis des espaces plus mystérieux ; aucun placement actif près de la salle d’expansion. | Exploration, circulations humides et sèches, passages vers les profondeurs. | Éventuel donjon physique ; distinct de la dimension Backrooms des objets perdus. |
|
||||
|
||||
La traversée est une infrastructure de circulation, pas un quota de bâtiments
|
||||
annexes. Ses sorties peuvent être isolées. **Le réseau d’égouts et de canalisations
|
||||
devient prioritaire autour de la salle**, avec plusieurs galeries qui y conduisent
|
||||
et un éclairage par la redstone. La trappe est conservée comme l’une des entrées,
|
||||
sans concentrer tout l’accès sur elle. Tracés, embranchements, alimentation de
|
||||
l’éclairage et raccords à la grande traversée restent à dessiner. Ces réseaux
|
||||
existent indépendamment d’une ville et peuvent se rejoindre ponctuellement.
|
||||
|
||||
Des ouvertures dans les parois de l’île et des tronçons suspendus dans le vide
|
||||
sont permis. Les transitions, parois, appuis ou suspensions et raccordements
|
||||
doivent former une construction cohérente : l’ouverture sur le vide ne justifie
|
||||
pas des morceaux détachés ou des circulations coupées par erreur. Cette direction
|
||||
ne modifie pas les piscines, égouts ou accès déjà présents dans les sauvegardes
|
||||
des versions livrées.
|
||||
|
||||
Pour Sanctuary Island, 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. Les bâtiments urbains pourront servir aux Lost Cities
|
||||
des continents, avec une sélection et une distribution encore à dessiner. Cette
|
||||
répartition ne modifie aucun identifiant et n’affirme pas que chaque nom correspond
|
||||
à un type de structure distinct déjà codé.
|
||||
|
||||
### Laboratoire de confinement animal — expansions
|
||||
|
||||
**Nouvelle structure souhaitée, réservée aux continents d’expansion :** un
|
||||
laboratoire avec des cellules ou enclos de confinement animal, comprenant des
|
||||
**Mooblooms**. Les espaces doivent rendre lisible cette ancienne activité
|
||||
expérimentale par leur architecture, leurs animaux et leurs installations,
|
||||
sans panneaux explicatifs. Cette direction ne définit aucune mécanique
|
||||
d’expérimentation sur les animaux.
|
||||
|
||||
Plan, accès, circulations entre enclos, ouverture des cellules et moyens de
|
||||
libérer les animaux restent à concevoir. L’extraction aérienne peut employer le
|
||||
montage retenu : animal embarqué dans un bateau, relié par une corde et soulevé
|
||||
par un bateau-poule. Il faut dessiner un accès qui rende cette sortie possible.
|
||||
Le **Booat**, nom en jeu du véhicule surnommé **bi-poule**, est un grand bateau à
|
||||
quatre, retenu parmi les mécaniques centrales. Il accueille quatre joueurs sur
|
||||
l’eau, puis deux joueurs et deux poules en vol, comme confirmé dans le
|
||||
[cahier du Booat](progression-et-integrations.md#booat). Sa fabrication emploie
|
||||
quatre bateaux et il possède des variantes de bois. Ses dimensions et son usage
|
||||
pour l’extraction doivent être pris en compte dans le dessin de ces accès ;
|
||||
aucun comportement de transport n’est implémenté ici.
|
||||
Variantes de Mooblooms,
|
||||
autres occupants, fréquence du laboratoire et contenu ne sont pas fixés ; sa
|
||||
présence n’est pas garantie sur chaque continent. Ce lieu ne s’ajoute pas à la
|
||||
sélection de Sanctuary Island.
|
||||
|
||||
« Laboratoire » désigne cette nouvelle structure, sans réactiver l’ancien choix
|
||||
de génération Laboratory. Aucun bâtiment, animal ou comportement n’est ajouté
|
||||
aux mondes par ce document.
|
||||
|
||||
### Reliefs et grandes rencontres des expansions
|
||||
|
||||
Les grands continents peuvent comporter des **océans avec des temples ou
|
||||
monuments sous-marins**, destinés à accueillir un futur **boss aquatique**.
|
||||
Un **volcan abritant un mob géant** constitue une autre direction de structure.
|
||||
Les deux rencontres restent à concevoir : aucun nom de mob, comportement,
|
||||
nombre, récompense ou condition de progression n’est fixé. Formes des reliefs,
|
||||
implantation, circulations et accès doivent être dessinés avec les structures.
|
||||
Ces lieux concernent les expansions, sans réintroduire les temples dans la
|
||||
génération de base de Sanctuary Island ni remplacer le donjon de l’île qui
|
||||
débloque l’accès aux Cavernes.
|
||||
|
||||
Les **Lost Cities peuvent déborder des petites îles de rayon 64 blocs**, soit
|
||||
un diamètre nominal de 128 blocs. Ce repère décrit le terrain envisagé, pas un
|
||||
nouveau preset de création de monde. L’emprise construite peut dépasser la rive
|
||||
avec des surplombs cohérents ; bâtiments, voies, raccords et structures porteuses
|
||||
doivent rester lisibles comme un même ensemble.
|
||||
|
||||
Cet assouplissement vise ces implantations urbaines d’expansion : il ne rétablit
|
||||
ni un forçage global de la génération, ni des villes sur l’île de départ. Les
|
||||
conditions de refus, les limites de débordement et la protection des emprises
|
||||
existantes restent à définir avant toute modification du générateur. Aucun
|
||||
placement supplémentaire n’est livré par ce document.
|
||||
|
||||
### Lieux flottants du monde initial
|
||||
|
||||
Ces sites appartiennent à la **génération initiale du monde**, avant toute
|
||||
expansion Galactium. « Déjà générés » signifie ici présents dans ce monde,
|
||||
mais encore inexplorés ; leur réalisation technique peut rester progressive
|
||||
avec le chargement des chunks. Cette direction ne demande ni charge d’expansion
|
||||
ni pré-génération massive de tous les chunks.
|
||||
|
||||
L’objectif envisagé est **un ou deux îlots suspendus riches**, avec un coffre
|
||||
enterré à découvrir. Leurs dimensions, leurs ressources et le nombre finalement
|
||||
retenu restent à concevoir. Des cascades peuvent servir d’accès vertical, mais
|
||||
elles ne sont pas obligatoires : elles peuvent tomber dans le vide sans exiger
|
||||
un terrain ou une île dessous.
|
||||
|
||||
Les **bateaux marchands se répartissent tout autour de Sanctuary, en périphérie**.
|
||||
Ils constituent la première voie prévue pour obtenir des villageois. Pour les
|
||||
ramener, les joueurs les embarquent dans des bateaux qu’ils relient par des
|
||||
cordes à leur bateau-poule : celui-ci les soulève et les transporte. Accès,
|
||||
nombre de navires, répartition des villageois et limites de traction restent à
|
||||
dessiner. Les **montgolfières contiennent des richesses**, dont la nature et la
|
||||
répartition ne sont pas encore fixées.
|
||||
|
||||
La première structure volante souhaitée près de Sanctuary, sur ou au-dessus de
|
||||
l’île, doit être dessinée avec le relief, les accès et la vue depuis l’arrivée.
|
||||
Le choix du type de structure et sa position restent ouverts ; cette découverte
|
||||
doit conserver la sensation de commencer dans un endroit naturel. Ces pistes
|
||||
ne constituent pas un catalogue à placer entièrement à chaque graine.
|
||||
|
||||
Le comportement fixe ou mobile des bateaux et ballons reste à définir ; aucun
|
||||
déplacement de véhicule n’est adopté par cette répartition. La présence souhaitée
|
||||
de marchands et de villageois ne livre pas de PNJ ni de système de commerce.
|
||||
Ces lieux pourraient faire découvrir des pages culinaires d’**It's Alive !**,
|
||||
des plans de construction ou des programmes sur disquette, en lien avec la
|
||||
progression à concevoir. Leur répartition, les conditions de déblocage et le
|
||||
contenu des coffres restent ouverts ; aucun butin n’est attribué par ce document.
|
||||
|
||||
**Les temples sont retirés de la génération de base.** Ils restent une idée de
|
||||
futures structures autour de Sanctuary, avec une inspiration du Nether encore
|
||||
à préciser. Ni leur forme, ni leur accès, ni leur contenu ne sont définis ou
|
||||
implémentés ici.
|
||||
|
||||
### Descendre vers les installations par une waystone
|
||||
|
||||
**Direction retenue :** trouver une waystone dans un accès naturel d’une île
|
||||
générée, puis l’utiliser comme ascenseur vers un niveau inférieur contenant
|
||||
un laboratoire ou une installation. La descente fait découvrir le lieu et
|
||||
l’appareil par leur usage. Le choix des îles, des accès et des étages reste à
|
||||
concevoir ; aucune quantité par île ni obligation d’en mettre dans chaque
|
||||
structure n’est fixée.
|
||||
|
||||
La même waystone possède deux usages : verticalement, sauter pour monter et
|
||||
s’accroupir pour descendre, sans devoir connaître l’étage au préalable ;
|
||||
horizontalement, rejoindre précisément une waystone connue de la même dimension,
|
||||
avec un coût élevé en XP croissant avec la distance. Ce sont les fonctions d’un
|
||||
même appareil, pas deux nouveaux paliers de progression. Alignement, portée,
|
||||
arrivée praticable et barèmes restent à préciser avec le
|
||||
[cahier de progression](progression-et-integrations.md).
|
||||
|
||||
Dessiner les arrivées, les retours et les circulations en cohérence avec la
|
||||
grande traversée et les autres accès. Ces ascenseurs complètent les passages
|
||||
souterrains et leurs découvertes ; ils ne suppriment pas le parcours du donjon.
|
||||
Ils restent une intention de génération future, sans modification de l’alpha
|
||||
publiée ou des sauvegardes existantes.
|
||||
|
||||
## Découvrir des usages concrets
|
||||
|
||||
**Direction retenue :** les lieux donnent à voir ce que les futurs appareils
|
||||
permettent de faire. Le spectaculaire vient des volumes, des circulations et
|
||||
du fonctionnement de l’installation : un effet visible doit pouvoir être relié
|
||||
à des blocs et à un programme que les joueurs pourront étudier puis reproduire.
|
||||
Les salles de machines doivent être plus grandes et montrer plusieurs systèmes
|
||||
de farming avec leurs ordinateurs. Leurs dimensions découlent des installations
|
||||
et des circulations à dessiner ; aucune taille ni liste obligatoire de fermes
|
||||
n’est arrêtée. **Aucun panneau explicatif ni texte tutoriel** : on apprend en
|
||||
regardant les blocs, en suivant les flux d’objets et les signaux redstone, puis
|
||||
en consultant et essayant les programmes. Les afficheurs montrent les données
|
||||
et l’état réel des systèmes ; ils ne deviennent pas des panneaux de cours.
|
||||
Les scènes suivantes sont des pistes de conception, pas des montages livrés ni
|
||||
une obligation de placer tous les appareils dans chaque lieu.
|
||||
|
||||
| Lieu | Scène proposée à dessiner | Ce que les joueurs peuvent en reprendre |
|
||||
| --- | --- | --- |
|
||||
| Atelier caché ou laboratoire | Plusieurs systèmes de farming avec leurs ordinateurs laissent suivre la production, les transferts et le stockage. Un montage proposé régule une réserve : coffre, comparateur, contrôleur, hopper et afficheur ; la sortie redstone commande le transfert selon un seuil. Terminal et disquette rendent le programme consultable. | Des systèmes à reproduire séparément ou à combiner dans leurs propres fermes. Le comparateur donne une mesure de 0 à 15, pas un inventaire exact ; types de fermes et raccords restent à choisir. |
|
||||
| Observatoire | Un afficheur intégré au poste d’observation présente un calendrier ou un rendez-vous céleste préparé, avec des données issues des sources serveur prévues. Le programme et les instruments expliquent l’usage de l’espace sans imposer un cours ou un dialogue. | Un poste d’observation ou un calendrier dans leurs propres constructions ; les données accessibles et les raccordements restent à définir. |
|
||||
| Grande salle d’expansion | Depuis l’entrée, les joueurs peuvent lire l’organisation du socle, des apports et du poste de commande. Terminal, contrôleur et afficheur permettent de préparer et suivre une opération une fois les conditions réunies ; les parties manquantes restent à choisir. | Une installation d’expansion reproductible ailleurs. L’écran présente l’état réel du projet, des charges et des apports validés ; il ne promet pas une carte exacte avant génération. |
|
||||
|
||||
Les disquettes trouvées pourraient conserver les programmes de ces installations.
|
||||
Leur état initial, leurs recettes, leur récupération et les conditions d’accès
|
||||
restent à choisir. **Les programmes sont partageables, duplicables et revendables**
|
||||
dans la direction retenue ; modalités de copie, supports et règles de vente
|
||||
restent à concevoir. Un programme partagé ne fournit ni les ressources ni les
|
||||
déblocages collectifs nécessaires à son fonctionnement. Les contrats techniques
|
||||
et les autres combinaisons sont dans le
|
||||
[cahier des machines](langage-et-machines.md#combinaisons-émergentes).
|
||||
|
||||
L’exploration peut intéresser celui qui bâtit, mine, programme ou combat. Aucun
|
||||
profil ne devient une classe obligatoire ; le fait de profiter d’une installation
|
||||
commune n’impose pas à chacun d’en écrire le programme. Le PvP fait partie des
|
||||
pratiques évoquées, mais ce cadrage ne fixe aucune règle de combat, de protection
|
||||
ou de propriété supplémentaire.
|
||||
|
||||
**Piste d’information : cartes au trésor.** Une carte pourrait orienter vers une
|
||||
île déjà présente dans le monde, site initial ou expansion déjà ouverte, avec
|
||||
une croix indiquant un coffre enterré dont l’emplacement est confirmé. La
|
||||
distinction entre site initial et chunks déjà chargés est celle définie ci-dessus.
|
||||
Elle donne une destination à explorer ; elle ne crée pas l’île, ne dévoile pas
|
||||
un futur continent non généré et ne promet pas un coffre réapprovisionné. Origine
|
||||
des cartes, accès à l’île, sélection des coffres et contenu restent à concevoir.
|
||||
|
||||
## 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. |
|
||||
| Répartition entre îles | Lost Cities surtout sur les continents ; lieux secrets et passages de Sanctuary Island à intégrer au relief, ainsi que les sites flottants du monde initial et la première structure volante proche de l’île. Préciser les fréquences et les éventuelles exceptions sans transformer cette priorité en quota garanti. |
|
||||
| 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 | Plusieurs accès par égouts et canalisations éclairés en redstone vers la salle, avec la trappe parmi eux. Les voies ferrées peuvent finir dans la nature sans bâtiment ; leurs interruptions doivent être lisibles et réparables. Dessiner les ouvertures dans les parois et les tronçons suspendus avec des raccords cohérents. |
|
||||
| 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 ; pour un montage, ses blocs, son programme, ses entrées et ses effets observables. Distinguer ce qui fonctionne, ce qui est à rééquiper et ce qui reste futur.
|
||||
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.
|
||||
|
||||
Le socle retient **le contrôleur, le terminal, l’afficheur et les disquettes**.
|
||||
Le bloc Assembleur est retiré ; le terminal prépare et assemble le code. Le
|
||||
contrôleur intègre son processeur et sa RAM, avec six faces de redstone. Pour
|
||||
l’expansion, casser un noyau libère désormais dans la conception une charge
|
||||
commune au serveur : le noyau disparaît sans être récupéré ni installé dans la
|
||||
machine. Dépôt et ancrage restent à concevoir ; aucun de ces objets n’est
|
||||
implémenté par ce document.
|
||||
|
||||
L’**afficheur programmable** est la surface visible de cet ensemble dans le
|
||||
monde : un rectangle plein de W × H panneaux offre 8H lignes sur toute la largeur
|
||||
W. Le contrôleur calcule et exécute, le terminal édite et assemble, la disquette
|
||||
transporte le programme et l’afficheur en présente les résultats. Coffres,
|
||||
hoppers et comparateurs fournissent des stocks, des transferts et des signaux
|
||||
réels. Leurs connexions et les accès aux données riches restent à définir ; le
|
||||
contrôleur n’acquiert pas une connaissance globale du monde.
|
||||
|
||||
La direction est maintenant de relier ces appareils par un langage secret
|
||||
utilisant l’écriture galactique, également lié aux expansions, aux waystones et
|
||||
à l’End. Le [cahier du langage et des machines](langage-et-machines.md) propose
|
||||
une première répartition des rôles et un usage redstone. L’assembleur direct,
|
||||
les six faces d’entrée/sortie et la RAM adressable sont retenus. La sauvegarde
|
||||
complète de l’état est la base de travail proposée ; les autres rôles, valeurs
|
||||
et règles d’exécution restent à préciser avant le code.
|
||||
|
||||
Pour rendre l’expansion diégétique, une
|
||||
[installation multibloc autour du contrôleur](langage-et-machines.md#programmer-les-fonctions-du-jeu-et-construire-une-expansion)
|
||||
est proposée. Des blocs ordinaires pourraient porter orientation, capacité et
|
||||
apport des ressources ; le programme préparerait et suivrait l’opération. Cette
|
||||
forme n’est pas encore validée et aucun coût ou plan n’est fixé.
|
||||
|
||||
Les [noyaux d’expansion](langage-et-machines.md#bloc-graine-et-météorites) peuvent
|
||||
être découverts dans les météorites, et éventuellement dans les installations
|
||||
anciennes. D’autres voies restent à définir. Une
|
||||
[interface Galactium](langage-et-machines.md#réserve-collective-et-interface-galactium)
|
||||
est proposée dans le terminal : réserve commune, projet local, ressources et
|
||||
suivi de l’opération, avec accès à l’éditeur assembleur. Consulter ou préparer un
|
||||
projet n’immobilise aucune charge. Galactium nomme l’ensemble du système
|
||||
d’expansion. Les droits d’utilisation de la réserve et la disposition de
|
||||
l’interface restent en discussion.
|
||||
|
||||
Avant de fixer le plan intérieur de la salle, décider :
|
||||
|
||||
- ce que chaque élément reçoit, conserve, exécute ou affiche ;
|
||||
- comment le terminal permet de programmer, assembler, enregistrer et charger le code ;
|
||||
- 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 ;
|
||||
- l’ancrage de l’installation et sa destination, ainsi que les règles de dépense des charges communes ;
|
||||
- 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 majeur devient un immense laboratoire caché dans Sanctuary Island.**
|
||||
Il contient un portail vers la dimension des **Cavernes**, le monde de minage,
|
||||
dont l’accès est éteint ou instable tant que son gardien n’est pas vaincu.
|
||||
Le choix reste ouvert entre **zombie géant et golem de pierre**. L’auteur
|
||||
indique pouvoir obtenir des sources et modèles de golems auprès d’un ami ;
|
||||
ils ne sont pas encore fournis ni intégrés. Ce lieu est distinct de la salle
|
||||
d’expansion Galactium et du laboratoire de confinement animal des expansions.
|
||||
|
||||
Le parcours retenu est : découvrir le laboratoire, l’explorer, vaincre le
|
||||
gardien, puis ouvrir durablement les portails de minage. Le rôle de l’objet de
|
||||
quête prévu auparavant reste à préciser : victoire déclenchant directement la
|
||||
stabilisation ou objet utilisé pour la confirmer. La disparition d’un objet ne
|
||||
doit pas rendre impossible le premier accès collectif. Un **message de réussite
|
||||
à tout le serveur**, présenté comme un achievement collectif, marque le premier
|
||||
déblocage ; formulation, image et lien éventuel avec les advancements restent
|
||||
à concevoir. Cette annonce ne crée pas un verrou par joueur.
|
||||
|
||||
**Piste proposée :** un golem lié à l’ancienne installation pourrait rendre
|
||||
visible la relation entre gardien et portail instable. Ce serait la trace d’une
|
||||
ancienne solution de contrôle, dont la fonction exacte reste à écrire ; cette
|
||||
piste ne décide pas le boss et ne transforme pas le lieu en réacteur nucléaire.
|
||||
|
||||
**Le déblocage est collectif et permanent à l’échelle du serveur.** Une fois
|
||||
cette étape accomplie, chaque joueur peut fabriquer son propre portail vers les
|
||||
Cavernes, y compris s’il rejoint le serveur plus tard. Il n’a pas à refaire
|
||||
individuellement le donjon ni à obtenir l’objet de quête pour chaque portail.
|
||||
La construction des portails, leur recette et leurs modalités d’activation
|
||||
restent à définir.
|
||||
|
||||
La stabilisation porte ici sur les portails vers cette dimension. Son sens
|
||||
cosmologique éventuel n’est pas fixé.
|
||||
|
||||
La préparation du donjon doit être possible avant cet accès, avec les ressources
|
||||
disponibles sur l’île. Le nécromancien et sa boule de cristal déjà prévus dans les
|
||||
Cavernes restent une rencontre ultérieure distincte : on ne doit pas devoir
|
||||
entrer dans cette dimension pour obtenir la clé de son premier accès. Aucun
|
||||
prérequis supplémentaire pour l’expansion des continents n’est décidé ici.
|
||||
|
||||
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 :** choisir entre zombie géant et golem de pierre, puis préciser sa relation au portail, ses comportements et les conditions de victoire. Les dix joueurs des raids ne deviennent pas un effectif requis pour ce combat.
|
||||
- **Objet de quête :** sa nature, la manière de stabiliser les portails et s’il est consommé, conservé ou installé.
|
||||
- **Activation collective :** geste qui valide l’étape pour tout le serveur ; devenir de l’objet après cette validation. La portée du déblocage et l’accès des nouveaux arrivants sont acquis.
|
||||
- **Persistance :** combat unique ou rejouable, partage du butin et récupération d’un objet perdu avant activation. Le déblocage déjà acquis reste permanent si l’objet ou le lieu est ensuite perdu ou 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.
|
||||
|
||||
### Raids instanciés — chantier futur distinct
|
||||
|
||||
**Le portail des raids se découvre sur une île générée par expansion.**
|
||||
Cette direction remplace son placement sur Sanctuary Island ; le laboratoire
|
||||
des Cavernes reste dans l’île initiale. Générer une île donne une nouvelle
|
||||
destination à explorer, puis l’accès au raid est à découvrir et débloquer.
|
||||
Les conditions précises, la fréquence du lieu et sa garantie éventuelle ne sont
|
||||
pas fixées : toutes les expansions ne reçoivent pas automatiquement un portail.
|
||||
Il faut y réunir **dix joueurs** pour lancer l’épreuve.
|
||||
**Un raid pour tout le serveur par semaine**
|
||||
reste la limite commune. Le lieu d’accès est physique et persistant ; la
|
||||
destination est un donjon difficile généré procéduralement dans une instance,
|
||||
en **mode aventure**, avec une palette de blocs propre à concevoir. L’inspiration
|
||||
donnée est celle des **Trial Chambers**, développée à une autre échelle ; elle
|
||||
ne fixe pas automatiquement leurs blocs, spawners ou règles de récompenses.
|
||||
|
||||
Le **butin est commun et matériel** : les joueurs le ramassent et le répartissent
|
||||
entre eux, avec une quantité à équilibrer pour dix. Il n’y a ni dix récompenses
|
||||
personnelles automatiques, ni partage égal imposé. Cela remplace l’ancienne
|
||||
direction de butin personnel pour ces raids seulement. Consommation du quota,
|
||||
déconnexions, échec, retour et réutilisation de l’instance restent dans le
|
||||
[chantier de progression](progression-et-integrations.md#raids-collectifs-instanciés).
|
||||
|
||||
Ce souhait ne remplace ni le donjon majeur qui débloque collectivement les
|
||||
Cavernes, ni les tunnels et systèmes de farming du monde persistant. Aucun raid,
|
||||
Indoor ou rythme de réinitialisation n’est implémenté par ce cahier.
|
||||
|
||||
## 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 | Grande salle essentielle et rééquipable ; réseau d’égouts et de canalisations avec plusieurs accès, sans piscine voisine. | Direction retenue ; trappe conservée, éclairage redstone, tracés et contenu initial à définir. Correspondance « salle de transformation » à confirmer. |
|
||||
| 3. Sélection | Sur l’île initiale : observatoire, salle d’expansion, atelier caché, grande traversée, train et rails abandonnés, laboratoire des Cavernes. Sur une expansion : accès secret aux raids. | Retenue ; bâtiments ordinaires écartés de l’île initiale, tracés sans bâtiment terminal permis, plans et distribution à définir. |
|
||||
| 4. Objet du donjon | Accès au monde de minage par victoire sur le gardien du laboratoire, puis stabilisation des portails. | Déblocage collectif permanent annoncé au serveur ; chacun peut ensuite fabriquer son portail. Zombie géant ou golem de pierre, objet et activation à définir. |
|
||||
| 5. Territoire | Arrivée naturelle, lieux secrets de l’île, Lost Cities surtout sur les continents ; quantités, espacements et différences entre les trois tailles. | Direction retenue ; valeurs à définir, sans redimensionnement pour la cible d’environ vingt joueurs. |
|
||||
| 6. Fiches des lieux | Plans, usages concrets des appareils, indices, matériaux et état d’abandon ; systèmes à reproduire. | À définir pour la sélection retenue ; cartes au trésor comme piste d’information. |
|
||||
| 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.
|
||||
-1275
File diff suppressed because it is too large
Load Diff
-1014
File diff suppressed because it is too large
Load Diff
@@ -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.22
|
||||
pack_version=0.1.0-alpha.22
|
||||
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
|
||||
@@ -0,0 +1,482 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"version": "beta.065",
|
||||
"tag_name": "notes/beta.065",
|
||||
"name": "Sanctuary beta.065 — Collisions des bateaux collectifs",
|
||||
"body_file": "notes/beta.065.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "d923f2011923b92c2293fc55e2411ae22868041baeeebbc1685b70db850c41ba"
|
||||
},
|
||||
{
|
||||
"version": "beta.066",
|
||||
"tag_name": "notes/beta.066",
|
||||
"name": "Sanctuary beta.066 — Cosmétiques animés sans doublon",
|
||||
"body_file": "notes/beta.066.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "421ff61e6fe26f447f021d9752ed18ce482279360ed439b73a09a969b7421728"
|
||||
},
|
||||
{
|
||||
"version": "beta.067",
|
||||
"tag_name": "notes/beta.067",
|
||||
"name": "Sanctuary beta.067 — Commandes du familier et vol du dragon",
|
||||
"body_file": "notes/beta.067.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "ef56ee59b46e4ef1482f1573e3e06f2ab67bcb3c5760b4d8eb038951902e8f30"
|
||||
},
|
||||
{
|
||||
"version": "beta.068",
|
||||
"tag_name": "notes/beta.068",
|
||||
"name": "Sanctuary beta.068 — Animation discrète du chargement",
|
||||
"body_file": "notes/beta.068.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "7bddf6557907b317db9c60c81e1f0a1d602de7fb272d863acdd4ede8128d9d3f"
|
||||
},
|
||||
{
|
||||
"version": "beta.069",
|
||||
"tag_name": "notes/beta.069",
|
||||
"name": "Sanctuary beta.069 — Les ordres reviennent sur H",
|
||||
"body_file": "notes/beta.069.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "14bf12033f762315c90610f7576323bcba92c0aa9c421468f0eb27c5e0f289bc"
|
||||
},
|
||||
{
|
||||
"version": "beta.070",
|
||||
"tag_name": "notes/beta.070",
|
||||
"name": "Sanctuary beta.070 — Textures intégrées et actives par défaut",
|
||||
"body_file": "notes/beta.070.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "5853fc49b5ac1c7570eca4b7982373ea8befcb28d6306c431ff4b22c5bee70c9"
|
||||
},
|
||||
{
|
||||
"version": "beta.071",
|
||||
"tag_name": "notes/beta.071",
|
||||
"name": "Sanctuary beta.071 — Personnalités et montures des familiers",
|
||||
"body_file": "notes/beta.071.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6383c360e4757f6ce4dd6a96ed016889a8be0f4061666381be493036b3efb0b4"
|
||||
},
|
||||
{
|
||||
"version": "beta.072",
|
||||
"tag_name": "notes/beta.072",
|
||||
"name": "Sanctuary beta.072 — Statues et bibliothèque de plans",
|
||||
"body_file": "notes/beta.072.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "c2b73bb6bf5a37d2dd6bfe014b4dce878c054dc0ba8ec92564d4e89d73cb0477"
|
||||
},
|
||||
{
|
||||
"version": "beta.073",
|
||||
"tag_name": "notes/beta.073",
|
||||
"name": "Sanctuary beta.073 — Duels publics, arènes et paris en objets",
|
||||
"body_file": "notes/beta.073.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "8573fec951584d4bff53f649eb0504fa439a7d361bcc8380eed6288d213f7a7f"
|
||||
},
|
||||
{
|
||||
"version": "beta.074",
|
||||
"tag_name": "notes/beta.074",
|
||||
"name": "Sanctuary beta.074 — Autonomie et ordres des familiers",
|
||||
"body_file": "notes/beta.074.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "2490f4e7e405d8f34a599037930b80f269a40ada70d337d0048173f681c8f548"
|
||||
},
|
||||
{
|
||||
"version": "beta.075",
|
||||
"tag_name": "notes/beta.075",
|
||||
"name": "Sanctuary beta.075 — Bibliothèque de plans centrée sur les fichiers locaux",
|
||||
"body_file": "notes/beta.075.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "e416620956e26ed3e9b640ee65a41de6131cb54cd10c2afa85f050653a9e5872"
|
||||
},
|
||||
{
|
||||
"version": "beta.076",
|
||||
"tag_name": "notes/beta.076",
|
||||
"name": "Sanctuary beta.076 — Navigation — livrée avec beta.077",
|
||||
"body_file": "notes/beta.076.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "f4c5bbf1d446499c041cbef9f13ca476bee44d59629b2413a310fea34a83e8b0"
|
||||
},
|
||||
{
|
||||
"version": "beta.077",
|
||||
"tag_name": "notes/beta.077",
|
||||
"name": "Sanctuary beta.077 — Tourbillon de nuages sous Sanctuary",
|
||||
"body_file": "notes/beta.077.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6a0efeed42776e2101ee88dd61b0c4ede7f63de530d19287dc42b0b375c774f8"
|
||||
},
|
||||
{
|
||||
"version": "beta.078",
|
||||
"tag_name": "notes/beta.078",
|
||||
"name": "Sanctuary beta.078 — Le menu devient Combats",
|
||||
"body_file": "notes/beta.078.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6d2fc0e8cbabb36ca4f5eee0f957b4396063dca00b0daa063c25abf3cc6cf57f"
|
||||
},
|
||||
{
|
||||
"version": "beta.079",
|
||||
"tag_name": "notes/beta.079",
|
||||
"name": "Sanctuary beta.079 — Ordres fiables et défense immédiate en solo",
|
||||
"body_file": "notes/beta.079.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "ac2825606bd1444298c7193c4ce6e4ac540b92c97211d54e61a02665bd80c248"
|
||||
},
|
||||
{
|
||||
"version": "beta.080",
|
||||
"tag_name": "notes/beta.080",
|
||||
"name": "Sanctuary beta.080 — Portée et vitesse liées à la progression",
|
||||
"body_file": "notes/beta.080.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "a2cebe33c9a3cb01c7cd9a6d252ba073d54130675728a1e9b1ac8f2b768ee089"
|
||||
},
|
||||
{
|
||||
"version": "beta.081",
|
||||
"tag_name": "notes/beta.081",
|
||||
"name": "Sanctuary beta.081 — Construction jusqu’à six blocs",
|
||||
"body_file": "notes/beta.081.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "9fee210fe96b8204c62d07bb88ec2153b2118136c3e4662a5dbf21280f444ad4"
|
||||
},
|
||||
{
|
||||
"version": "beta.082",
|
||||
"tag_name": "notes/beta.082",
|
||||
"name": "Sanctuary beta.082 — Commandes des météos Sanctuary",
|
||||
"body_file": "notes/beta.082.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "d45355e5b5bcd08091e2c9c0cbae7027771ec226ecf45f012a872a51c3ed0557"
|
||||
},
|
||||
{
|
||||
"version": "beta.083",
|
||||
"tag_name": "notes/beta.083",
|
||||
"name": "Sanctuary beta.083 — Menus organisés et découvertes réunies",
|
||||
"body_file": "notes/beta.083.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "203edaec84645f0d3434ed9cb57ac8345fe1ca539f5e578c5f8e70dbe64d0b72"
|
||||
},
|
||||
{
|
||||
"version": "beta.084",
|
||||
"tag_name": "notes/beta.084",
|
||||
"name": "Sanctuary beta.084 — Clé dorée, Fût et Fourneau",
|
||||
"body_file": "notes/beta.084.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "821c3c2bac30726c63bd6451b59efd5e12ab8821a5cf0eade3bc17383506f38b"
|
||||
},
|
||||
{
|
||||
"version": "beta.085",
|
||||
"tag_name": "notes/beta.085",
|
||||
"name": "Sanctuary beta.085 — Une façade complète pour le Fourneau",
|
||||
"body_file": "notes/beta.085.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "cbe0f5468925999ca0f41468487349c1b5f99ad1c4346cd7b836677271c996d5"
|
||||
},
|
||||
{
|
||||
"version": "beta.086",
|
||||
"tag_name": "notes/beta.086",
|
||||
"name": "Sanctuary beta.086 — Textures raccordées du Fût",
|
||||
"body_file": "notes/beta.086.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "3f6a6803a0029278b2a5cdcc8708e49f1f62df5d836ffc22c7cc053018a652cd"
|
||||
},
|
||||
{
|
||||
"version": "beta.087",
|
||||
"tag_name": "notes/beta.087",
|
||||
"name": "Sanctuary beta.087 — Super pistons normaux et gluants",
|
||||
"body_file": "notes/beta.087.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "b00d0389ac6a47124031d7d768834ada51e7b76e2ce9eb6db8d31e72513ff57b"
|
||||
},
|
||||
{
|
||||
"version": "beta.088",
|
||||
"tag_name": "notes/beta.088",
|
||||
"name": "Sanctuary beta.088 — Navigation du Fût sans recentrer la souris",
|
||||
"body_file": "notes/beta.088.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "fb327e5bddc57a490c8751022423d2a60545239c29ede0ece07d1d4943a0ebce"
|
||||
},
|
||||
{
|
||||
"version": "beta.089",
|
||||
"tag_name": "notes/beta.089",
|
||||
"name": "Sanctuary beta.089 — Correction du crash au contact des super pistons",
|
||||
"body_file": "notes/beta.089.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "039e97c2ff370f020bab8b0711a2b8f1ef1ddaa0e011723165e5b6a0ef1c6ad2"
|
||||
},
|
||||
{
|
||||
"version": "beta.090",
|
||||
"tag_name": "notes/beta.090",
|
||||
"name": "Sanctuary beta.090 — Passage à Minecraft Java 26.3",
|
||||
"body_file": "notes/beta.090.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "914e301e6b3734d3268a91adcf9b26cff2da81ca4423019b52107ab68781934a"
|
||||
},
|
||||
{
|
||||
"version": "beta.091",
|
||||
"tag_name": "notes/beta.091",
|
||||
"name": "Sanctuary beta.091 — Interagir avec les animaux embarqués",
|
||||
"body_file": "notes/beta.091.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "00e834683446ddc402748fcfa0e56c2ebe364c20be5cdfe6a59fea5ef5000de3"
|
||||
},
|
||||
{
|
||||
"version": "beta.092",
|
||||
"tag_name": "beta.092",
|
||||
"name": "Sanctuary beta.092 — Clic molette des super pistons",
|
||||
"body_file": "notes/beta.092.md",
|
||||
"source_tag_exact": true,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "0e98172117eb057380280dc3463db7e4a224304fe9d83a4ccfc35a223b18afb7"
|
||||
},
|
||||
{
|
||||
"version": "beta.093",
|
||||
"tag_name": "notes/beta.093",
|
||||
"name": "Sanctuary beta.093 — Recherche créative sans accès graphique en arrière-plan",
|
||||
"body_file": "notes/beta.093.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "eb30571a8246c44e511afcaff49e33141daa5ed85c1655bdcd2830a3f5b7cc96"
|
||||
},
|
||||
{
|
||||
"version": "beta.094",
|
||||
"tag_name": "notes/beta.094",
|
||||
"name": "Sanctuary beta.094 — Les foyers du Fourneau s’allument",
|
||||
"body_file": "notes/beta.094.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "4e0f68763d83bef5873fb059cd5babe840b217d58e84eb98d346e0f42e790f7d"
|
||||
},
|
||||
{
|
||||
"version": "beta.095",
|
||||
"tag_name": "notes/beta.095",
|
||||
"name": "Sanctuary beta.095 — Bois raccordé sur la tige des super pistons",
|
||||
"body_file": "notes/beta.095.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "76b902e563814986ed9098264475224b213dce881f6db2cc90240d07f572a42b"
|
||||
},
|
||||
{
|
||||
"version": "beta.096",
|
||||
"tag_name": "notes/beta.096",
|
||||
"name": "Sanctuary beta.096 — Métabli et construction guidée",
|
||||
"body_file": "notes/beta.096.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "6ad6585286fefa500d03a0545b23dd1e82f111e4c2ed81b8f1612cc8f7d64472"
|
||||
},
|
||||
{
|
||||
"version": "beta.097",
|
||||
"tag_name": "notes/beta.097",
|
||||
"name": "Sanctuary beta.097 — Temples garantis et aperçus texturés",
|
||||
"body_file": "notes/beta.097.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "fbaa9c89c051d04441002ed071cd9df5f5ec37cc98decf2611d5a8754bc6ef75"
|
||||
},
|
||||
{
|
||||
"version": "beta.098",
|
||||
"tag_name": "notes/beta.098",
|
||||
"name": "Sanctuary beta.098 — Construire un plan en créatif",
|
||||
"body_file": "notes/beta.098.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "4e702025e0f60ea43a99b9f8846e77ae411f27588d47f20d01b0a00c1e0dafef"
|
||||
},
|
||||
{
|
||||
"version": "beta.099",
|
||||
"tag_name": "notes/beta.099",
|
||||
"name": "Sanctuary beta.099 — Catalogue visuel du Métabli et gestes K",
|
||||
"body_file": "notes/beta.099.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "53ee7a482b7b2a0965ffea01d118d1e321fa19348edebe795a70ba7a35fd2586"
|
||||
},
|
||||
{
|
||||
"version": "beta.100",
|
||||
"tag_name": "notes/beta.100",
|
||||
"name": "Sanctuary beta.100 — Exploration, statues découvertes et promenade",
|
||||
"body_file": "notes/beta.100.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "dd55eab663b86ea287d4f0952c2ac07822c532450afd7f7abbf6b1da89c6cfa8"
|
||||
},
|
||||
{
|
||||
"version": "beta.101",
|
||||
"tag_name": "notes/beta.101",
|
||||
"name": "Sanctuary beta.101 — Atterrissage sans faux coup pour les volants",
|
||||
"body_file": "notes/beta.101.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "32c1cee580d78b6e3b9990fbdd1b4a3f4aec16d2da58b2ed21496381a8365c67"
|
||||
},
|
||||
{
|
||||
"version": "beta.102",
|
||||
"tag_name": "notes/beta.102",
|
||||
"name": "Sanctuary beta.102 — Seize couleurs de briques et d’argile",
|
||||
"body_file": "notes/beta.102.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "34a469c081b9e7aa9830c7bb42310d103f590a8b43857af4d543d6f5041239fb"
|
||||
},
|
||||
{
|
||||
"version": "beta.103",
|
||||
"tag_name": "notes/beta.103",
|
||||
"name": "Sanctuary beta.103 — Rangement des couleurs par séries",
|
||||
"body_file": "notes/beta.103.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "1a0757e25d1ea16f0d59f0e170e9e7f08cc1143158122d6345fed2cd0e64ecc0"
|
||||
},
|
||||
{
|
||||
"version": "beta.104",
|
||||
"tag_name": "notes/beta.104",
|
||||
"name": "Sanctuary beta.104 — Argiles grise et noire plus distinctes",
|
||||
"body_file": "notes/beta.104.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "59104848b12475cd3e80700cb70a27e341e2b62c561c3a1b63c1495cd53a3350"
|
||||
},
|
||||
{
|
||||
"version": "beta.105",
|
||||
"tag_name": "notes/beta.105",
|
||||
"name": "Sanctuary beta.105 — Saisons, panoramas et chapeaux vivants",
|
||||
"body_file": "notes/beta.105.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "5b53c5dfbcffbe533bb606e21843235fb65c2ca9c4950856b8d2bef27afc3d74"
|
||||
},
|
||||
{
|
||||
"version": "beta.106",
|
||||
"tag_name": "notes/beta.106",
|
||||
"name": "Sanctuary beta.106 — Atelier d’argile et modèles 3D",
|
||||
"body_file": "notes/beta.106.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "822e2f9d11618216053a89c580bc87a7a95c3e6ee894e516d02193f94fd6325e"
|
||||
},
|
||||
{
|
||||
"version": "beta.107",
|
||||
"tag_name": "notes/beta.107",
|
||||
"name": "Sanctuary beta.107 — Œufs de reproduction et de générateurs",
|
||||
"body_file": "notes/beta.107.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "95f163f0f5f6392a0a6e92cd76dbfa0275081f76032e654a0b1966204b0bd3b4"
|
||||
},
|
||||
{
|
||||
"version": "beta.108",
|
||||
"tag_name": "notes/beta.108",
|
||||
"name": "Sanctuary beta.108 — La neige s’accumule en blocs",
|
||||
"body_file": "notes/beta.108.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "65f3e08a8c5300cf1d1d3ff7cddf9e75f38b84d42177d3f2f177eb46361323b0"
|
||||
},
|
||||
{
|
||||
"version": "beta.109",
|
||||
"tag_name": "notes/beta.109",
|
||||
"name": "Sanctuary beta.109 — Fonte saisonnière de la neige",
|
||||
"body_file": "notes/beta.109.md",
|
||||
"source_tag_exact": false,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "189304161a5bc3e6a182f0e07e62d879a043943e9db37e4354197535b7268d81"
|
||||
},
|
||||
{
|
||||
"version": "beta.110",
|
||||
"tag_name": "beta.110",
|
||||
"name": "Sanctuary beta.110",
|
||||
"body_file": "notes/beta.110.md",
|
||||
"source_tag_exact": true,
|
||||
"draft": false,
|
||||
"prerelease": true,
|
||||
"body_sha256": "fb7bf23daa7a16ba9c52f180f442ca2d60eaa33eada946e1d721398ffd6c823d"
|
||||
}
|
||||
]
|
||||
+1092
File diff suppressed because it is too large
Load Diff
@@ -1,532 +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')
|
||||
}
|
||||
}
|
||||
|
||||
// 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' : '22'))
|
||||
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').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 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: 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('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) }
|
||||
}
|
||||
-35
@@ -1,35 +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 check of production render mixins in the actual client, without opening a save. */
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-179
@@ -1,179 +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();
|
||||
require(after.options().equals(before.options()), "Size must preserve seed, structures and bonus chest");
|
||||
for (var dimension : List.of(LevelStem.NETHER, LevelStem.END)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
-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; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
-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
-1034
File diff suppressed because it is too large
Load Diff
-1083
File diff suppressed because it is too large
Load Diff
-409
@@ -1,409 +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.LostCityPiece;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPlan;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityStructures;
|
||||
import java.io.IOException;
|
||||
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.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
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.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in native engine proof on a development world, including a separate-process reload. */
|
||||
public final class LostCityWorldGameTests {
|
||||
@GameTest(maxTicks = 6000)
|
||||
public void cityDistrictsAreNativePersistentStructures(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"Lost City checks require the unified generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 14, "Lost City must use new generation 14");
|
||||
if (Boolean.getBoolean("sanctuary.test.expansionReload")) {
|
||||
reload(helper, generator);
|
||||
return;
|
||||
}
|
||||
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<>();
|
||||
int phase;
|
||||
long poolPlacedTick;
|
||||
String childId;
|
||||
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator;
|
||||
session = ExpansionRuntime.session(level); report = report(level);
|
||||
}
|
||||
|
||||
void advance() {
|
||||
try {
|
||||
if (phase == 0) {
|
||||
report.put("archetypes", verifySavedArchetypes(helper));
|
||||
verifyLegacyCodec(helper, generator);
|
||||
placePoolFixture(helper, generator);
|
||||
poolPlacedTick = helper.getTick();
|
||||
var root = session.island("sanctuary");
|
||||
helper.assertTrue(root.generation() == 14, "New root must keep generation 14 in its journal");
|
||||
sites.add(observe(helper, generator, root));
|
||||
var prospects = new ArrayList<Map<String, Object>>();
|
||||
report.put("city_prospects", prospects);
|
||||
for (String id : List.of("city_test", "city_harbor", "city_gardens", "city_heights", "city_works")) {
|
||||
var candidate = session.candidate(id, "sanctuary", "east", "lost_city", 256, "natural", 2048);
|
||||
try {
|
||||
var plan = generator.lostCityPlan(candidate);
|
||||
prospects.add(Map.of("id", id, "seed", candidate.seed(), "diameter", candidate.diameter(),
|
||||
"districts", plan.districts().size(), "read_only", true));
|
||||
if (!plan.isEmpty()) { childId = id; break; }
|
||||
} finally { generator.discardUnpublishedContext(candidate); }
|
||||
}
|
||||
helper.assertTrue(childId != null, "Five bounded read-only prospects must find a naturally supported city reference");
|
||||
var child = session.create(childId, "sanctuary", "east", "lost_city", 256, "natural", 2048);
|
||||
helper.assertTrue(child.generation() == 14 && child.climate().equals("lost_city"),
|
||||
"City expansion must have its own saved generation and climate");
|
||||
phase = 1;
|
||||
}
|
||||
if (phase == 1 && session.island(childId).status().equals("ready") && helper.getTick() - poolPlacedTick >= 60) {
|
||||
report.put("native_pool_fixture", verifyPoolFixture(helper, true));
|
||||
forcePoolFixture(level, false);
|
||||
sites.add(observe(helper, generator, session.island(childId)));
|
||||
report.put("sites", sites);
|
||||
var first = generator.lostCityPlan(session.island("sanctuary")).districts().getFirst().parcels().getFirst();
|
||||
var marker = new BlockPos(first.minX(), first.floorY() + 1, first.minZ());
|
||||
level.setBlock(marker, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", List.of(marker.getX(), marker.getY(), marker.getZ()));
|
||||
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))));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush city chunks, deferred loot and marker before restart");
|
||||
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 City integration phase {}: {}", phase, session.progress());
|
||||
helper.runAtTickTime(helper.getTick() + 10, this::advance);
|
||||
} catch (Exception exception) {
|
||||
forcePoolFixture(level, false);
|
||||
report.put("passed", false); report.put("failure", exception.toString()); report.put("sites", sites);
|
||||
try { write(level, report, false); } catch (IOException ignored) { /* Keep the original test failure. */ }
|
||||
throw new IllegalStateException("Lost City native integration failed", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deliberately isolated in development void, apart from the two naturally planned city witnesses.
|
||||
private static final int POOL_X = 6000, POOL_Z = 0, POOL_Y = 366;
|
||||
private static void forcePoolFixture(ServerLevel level, boolean forced) {
|
||||
for (int x = (POOL_X - 1) >> 4; x <= (POOL_X + 17) >> 4; x++)
|
||||
for (int z = (POOL_Z - 1) >> 4; z <= (POOL_Z + 17) >> 4; z++) level.setChunkForced(x, z, forced);
|
||||
}
|
||||
private static void placePoolFixture(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
forcePoolFixture(level, true);
|
||||
var parcel = new LostCityPlan.Parcel(LostCityPlan.Kind.POOL, POOL_X, POOL_Z, 17, 17, POOL_Y, 0, 0, 42, 8, new int[0]);
|
||||
var piece = new LostCityPiece(parcel);
|
||||
piece.getBoundingBox().intersectingChunks().forEach(pos -> {
|
||||
level.getChunk(pos.x(), pos.z());
|
||||
var clip = new net.minecraft.world.level.levelgen.structure.BoundingBox(pos.x() * 16, 0, pos.z() * 16,
|
||||
pos.x() * 16 + 15, 383, pos.z() * 16 + 15);
|
||||
piece.postProcess(level, level.structureManager(), generator, net.minecraft.util.RandomSource.create(42), clip, pos, BlockPos.ZERO);
|
||||
});
|
||||
level.scheduleTick(new BlockPos(POOL_X + 8, POOL_Y - 1, POOL_Z + 10), net.minecraft.world.level.material.Fluids.WATER, 5);
|
||||
}
|
||||
private static Map<String, Object> verifyPoolFixture(GameTestHelper helper, boolean requireTicks) {
|
||||
var level = helper.getLevel();
|
||||
var witness = new BlockPos(POOL_X + 8, POOL_Y - 1, POOL_Z + 10);
|
||||
if (requireTicks) {
|
||||
helper.assertTrue(level.shouldTickBlocksAt(ChunkPos.containing(witness).pack()), "Pool fixture must actually tick in Minecraft");
|
||||
helper.assertTrue(!level.getFluidTicks().hasScheduledTick(witness, net.minecraft.world.level.material.Fluids.WATER),
|
||||
"The explicitly scheduled water tick must have executed");
|
||||
}
|
||||
int water = 0;
|
||||
for (int x = -1; x <= 17; x++) for (int z = -1; z <= 17; z++) for (int y = -9; y <= 1; y++) {
|
||||
var pos = new BlockPos(POOL_X + x, POOL_Y + y, POOL_Z + z);
|
||||
if (!level.getFluidState(pos).isEmpty()) {
|
||||
water++;
|
||||
helper.assertTrue(x >= 4 && x <= 12 && z >= 5 && z <= 12 && y >= -3 && y <= -1,
|
||||
"Native pool water escaped its intended basin after ticks or reload: " + pos);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(water >= 180, "The native pool lost its retained water");
|
||||
for (int step = 1; step <= 3; step++) helper.assertTrue(level.getBlockState(
|
||||
new BlockPos(POOL_X + 8, POOL_Y - step, POOL_Z + step + 4)).is(net.minecraft.tags.BlockTags.STAIRS),
|
||||
"Native submerged access steps must survive fluid ticks");
|
||||
return Map.of("scope", "Isolated native piece fixture, separate from random city placement", "retained_water_cells", water,
|
||||
"scheduled_water_tick_executed", requireTicks, "contained", true, "steps_preserved", true);
|
||||
}
|
||||
|
||||
private static Map<String, Object> observe(GameTestHelper helper, SanctuaryChunkGenerator generator, ExpansionIsland island) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
LostCityPlan plan = generator.lostCityPlan(island);
|
||||
helper.assertTrue(!plan.isEmpty(), "This reference seed must demonstrate an accepted city district on " + island.id());
|
||||
var district = plan.districts().getFirst();
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
var holder = registry.getOrThrow(LostCityStructures.KEY);
|
||||
var startChunk = level.getChunkSource().getChunk(district.startChunkX(), district.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
StructureStart start = startChunk.getStartForStructure(holder.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted district must have a native StructureStart");
|
||||
helper.assertTrue(start.getPieces().size() == district.parcels().size(), "Every accepted parcel must be saved as a native piece");
|
||||
Map<Long, LevelChunk> chunks = new TreeMap<>();
|
||||
for (var piece : start.getPieces()) piece.getBoundingBox().intersectingChunks().forEach(pos -> {
|
||||
if (!chunks.containsKey(pos.pack())) {
|
||||
var chunk = level.getChunkSource().getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "City inspection requires FULL chunks");
|
||||
chunks.put(pos.pack(), (LevelChunk) chunk);
|
||||
}
|
||||
});
|
||||
helper.assertTrue(chunks.size() <= 81, "One district is bounded; the check must not generate an entire island");
|
||||
for (var chunk : chunks.values()) helper.assertTrue(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 decoded = StructureStart.loadStaticStart(context, tag, island.seed());
|
||||
helper.assertTrue(decoded != null && decoded.isValid() && decoded.createTag(context, decoded.getChunkPos()).equals(tag),
|
||||
"Native start and complete parcel parameters must round-trip through saved NBT");
|
||||
Map<String, Long> blocks = new TreeMap<>();
|
||||
Map<String, Integer> kinds = new TreeMap<>();
|
||||
List<Map<String, Object>> chests = new ArrayList<>();
|
||||
int biomeWitnesses = 0, interiorWitnesses = 0;
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof LostCityPiece, "Lost City must load the registered native parcel type");
|
||||
var parcel = ((LostCityPiece) piece).parcel();
|
||||
kinds.merge(parcel.kind().name(), 1, Integer::sum);
|
||||
var box = piece.getBoundingBox();
|
||||
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 state = level.getBlockState(pos);
|
||||
if (!state.isAir()) blocks.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1L, Long::sum);
|
||||
if (parcel.kind().building() && y >= parcel.floorY()) helper.assertTrue(
|
||||
!BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore"),
|
||||
"Native ore features must not replace a city's masonry after structure placement");
|
||||
helper.assertTrue(!state.is(Blocks.SPAWNER), "City buildings contain no zombie spawner");
|
||||
if (state.is(Blocks.CHEST) && level.getBlockEntity(pos) instanceof RandomizableContainerBlockEntity chest
|
||||
&& chest.getLootTable() != null) {
|
||||
chests.add(Map.of("position", List.of(x, y, z), "table", chest.getLootTable().identifier().toString(), "seed", chest.getLootTableSeed()));
|
||||
}
|
||||
}
|
||||
var centre = new BlockPos(parcel.minX() + parcel.width() / 2, parcel.floorY() + 1, parcel.minZ() + parcel.depth() / 2);
|
||||
if (level.getBiome(centre).is(SanctuaryMod.id("lost_city"))) biomeWitnesses++;
|
||||
var entrance = entrance(parcel);
|
||||
if (parcel.kind().building() && level.getBlockState(entrance).isAir()
|
||||
&& level.getBlockState(entrance.above()).isAir() && level.getBlockState(entrance.below()).isSolid()) interiorWitnesses++;
|
||||
var monsters = generator.getMobsAt(level, level.structureManager(), MobCategory.MONSTER, centre).unwrap();
|
||||
helper.assertTrue(!monsters.isEmpty() && monsters.stream().allMatch(entry -> entry.value().type() == EntityTypes.ZOMBIE),
|
||||
"The effective native monster spawn list inside a city piece contains only vanilla zombies");
|
||||
}
|
||||
helper.assertTrue(biomeWitnesses > 0, "The city must exist as a real saved biome, not only as placed buildings");
|
||||
helper.assertTrue(interiorWitnesses > 0, "At least one observed contemporary building has a two-block-high traversable interior");
|
||||
helper.assertTrue(blocks.getOrDefault("minecraft:stone", 0L) > 0 && blocks.getOrDefault("minecraft:smooth_stone", 0L) > 0
|
||||
&& blocks.getOrDefault("minecraft:stone_bricks", 0L) > 0, "The actual city contains stone, smooth stone and restrained stone bricks");
|
||||
helper.assertTrue(!chests.isEmpty(), "An observed building must contain unconsumed deferred native loot");
|
||||
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 district contains no generated 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), "The native locate path finds Lost City");
|
||||
helper.assertTrue(!LostCityStructures.isPlacing(), "The scoped decoration bypass must be cleared after placement");
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("island", island.id()); result.put("generation", island.generation());
|
||||
result.put("planned_districts", plan.districts().size()); result.put("start_chunk", List.of(start.getChunkPos().x(), start.getChunkPos().z()));
|
||||
result.put("full_chunks", chunks.size()); result.put("parcel_kinds", kinds); result.put("blocks", blocks);
|
||||
result.put("native_start_sha256", hash(tag.toString())); result.put("unopened_chests", chests);
|
||||
result.put("biome_witnesses", biomeWitnesses); result.put("traversable_interiors", interiorWitnesses);
|
||||
result.put("native_zombie_only_monster_spawns", true); result.put("native_locate", List.of(located.getFirst().getX(), located.getFirst().getY(), located.getFirst().getZ()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> verifySavedArchetypes(GameTestHelper helper) {
|
||||
var context = StructurePieceSerializationContext.fromLevel(helper.getLevel());
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
for (var kind : LostCityPlan.Kind.values()) for (int facing = 0; facing < 4; facing++) {
|
||||
var parcel = new LostCityPlan.Parcel(kind, -32, 48, 17, 17, 220, kind.building() ? 3 : 1,
|
||||
facing, 0xA14C17L + kind.ordinal(), 4, new int[0]);
|
||||
var piece = new LostCityPiece(parcel);
|
||||
var tag = piece.createTag(context);
|
||||
var decoded = new LostCityPiece(context, tag);
|
||||
helper.assertTrue(decoded.createTag(context).equals(tag), "Every archetype and orientation must persist without changing parcel fields");
|
||||
var original = render(helper, parcel); var reloaded = render(helper, decoded.parcel());
|
||||
helper.assertTrue(!original.isEmpty() && original.equals(reloaded), "Reloaded parcels render exactly the same blocks and deferred loot seeds");
|
||||
results.add(Map.of("kind", kind.name(), "facing", facing, "positions", original.size(), "sha256", hash(original.toString())));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static BlockPos entrance(LostCityPlan.Parcel parcel) {
|
||||
int x = parcel.width() / 2, z = 1;
|
||||
int rotatedX = switch (parcel.facing()) {
|
||||
case 1 -> parcel.width() - 1 - z;
|
||||
case 2 -> parcel.width() - 1 - x;
|
||||
case 3 -> z;
|
||||
default -> x;
|
||||
};
|
||||
int rotatedZ = switch (parcel.facing()) {
|
||||
case 1 -> x;
|
||||
case 2 -> parcel.depth() - 1 - z;
|
||||
case 3 -> parcel.depth() - 1 - x;
|
||||
default -> z;
|
||||
};
|
||||
return new BlockPos(parcel.minX() + rotatedX, parcel.floorY() + 1, parcel.minZ() + rotatedZ);
|
||||
}
|
||||
|
||||
private static Map<String, String> render(GameTestHelper helper, LostCityPlan.Parcel parcel) {
|
||||
Map<String, String> rendered = new TreeMap<>();
|
||||
LostCityRenderer.render(parcel, new LostCityRenderer.BlockWriter() {
|
||||
private void put(int x, int y, int z, String value) {
|
||||
helper.assertTrue(x >= 0 && x < parcel.width() && z >= 0 && z < parcel.depth()
|
||||
&& parcel.floorY() + y >= parcel.minY() && parcel.floorY() + y <= parcel.maxY(),
|
||||
"Every renderer write must fit the persisted parcel bounds: " + parcel.kind() + " at " + x + "," + y + "," + z);
|
||||
rendered.put(x + "," + y + "," + z, value);
|
||||
}
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
helper.assertTrue(!state.is(Blocks.SPAWNER), "No original archetype emits a spawner");
|
||||
put(x, y, z, state.toString());
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
put(x, y, z, "loot:" + table.identifier() + ":" + seed);
|
||||
}
|
||||
});
|
||||
return rendered;
|
||||
}
|
||||
|
||||
private static void verifyLegacyCodec(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, helper.getLevel().registryAccess());
|
||||
JsonObject json = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
json.addProperty("type", "sanctuary:island_v13");
|
||||
json.getAsJsonObject("biome_source").addProperty("type", "sanctuary:island_v13");
|
||||
var old = (SanctuaryChunkGenerator) ChunkGenerator.CODEC.parse(ops, json).getOrThrow();
|
||||
helper.assertTrue(old.rootGeneration() == 13 && old.journalDirectory().equals("sanctuary-world-v13"),
|
||||
"The registered generation-13 codec keeps its journal and never enables city generation");
|
||||
helper.assertTrue(old.lostCityPlan(ExpansionIsland.origin(generator.initialDiameter(), helper.getLevel().getSeed(), 13)).isEmpty(),
|
||||
"Existing generation-13 worlds never acquire a city plan");
|
||||
}
|
||||
|
||||
private static void reload(GameTestHelper helper, SanctuaryChunkGenerator generator) {
|
||||
var level = helper.getLevel(); var result = report(level);
|
||||
try {
|
||||
JsonObject previous = JsonParser.parseString(Files.readString(path(level, false))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(), "Reload requires a completed first process");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Native city 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 read the exact same saved development world and seed");
|
||||
var session = ExpansionRuntime.session(level);
|
||||
helper.assertTrue(previous.get("islands").equals(ExpansionIsland.CODEC.listOf().encodeStart(JsonOps.INSTANCE, session.islands()).getOrThrow()),
|
||||
"The ready root and Lost City expansion identities must persist across process restart");
|
||||
helper.assertTrue(hash(Files.readString(journal(level, generator))).equals(previous.get("journal_sha256").getAsString()),
|
||||
"Reload must preserve the existing expansion journal bytes");
|
||||
var witness = previous.getAsJsonArray("gold_witness");
|
||||
var gold = new BlockPos(witness.get(0).getAsInt(), witness.get(1).getAsInt(), witness.get(2).getAsInt());
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "A saved edit inside the city must survive without structure regeneration");
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(LostCityStructures.KEY);
|
||||
int savedPieces = 0, deferredChests = 0;
|
||||
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(), "Native start must load from the existing saved chunk");
|
||||
helper.assertTrue(hash(start.createTag(context, start.getChunkPos()).toString()).equals(site.get("native_start_sha256").getAsString()),
|
||||
"Saved native structure pieces, layout and decoration seeds must remain byte-equivalent after restart");
|
||||
savedPieces += start.getPieces().size();
|
||||
for (var chestElement : site.getAsJsonArray("unopened_chests")) {
|
||||
var saved = chestElement.getAsJsonObject(); var at = saved.getAsJsonArray("position");
|
||||
var chestPos = new BlockPos(at.get(0).getAsInt(), at.get(1).getAsInt(), at.get(2).getAsInt());
|
||||
level.getChunk(chestPos);
|
||||
helper.assertTrue(level.getBlockEntity(chestPos) instanceof RandomizableContainerBlockEntity,
|
||||
"Deferred chest block entity must survive native region storage");
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestPos);
|
||||
helper.assertTrue(chest.getLootTable() != null && chest.getLootTable().identifier().toString().equals(saved.get("table").getAsString())
|
||||
&& chest.getLootTableSeed() == saved.get("seed").getAsLong(), "Unopened loot table and seed must remain unconsumed after restart");
|
||||
deferredChests++;
|
||||
}
|
||||
}
|
||||
var readback = new ExpansionJournal(journal(level, generator).getParent(), level.getSeed(), generator.initialDiameter(), 14);
|
||||
helper.assertTrue(readback.healthy() && readback.islands().equals(session.islands()), "The generation-14 journal reopens with its exact immutable identities");
|
||||
result.put("saved_native_pieces", savedPieces); result.put("preserved_deferred_chests", deferredChests);
|
||||
result.put("native_pool_fixture", verifyPoolFixture(helper, false));
|
||||
result.put("gold_witness_preserved", true); result.put("journal_preserved", true); result.put("passed", true);
|
||||
write(level, result, true); helper.succeed();
|
||||
} catch (Exception exception) {
|
||||
result.put("passed", false); result.put("failure", exception.toString());
|
||||
try { write(level, result, true); } catch (IOException ignored) { /* Keep the original failure. */ }
|
||||
throw new IllegalStateException("Lost City native reload failed", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> report(ServerLevel level) {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("world_directory", level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().toString());
|
||||
report.put("scope", "Actual native district generation on a fresh development root and one activated city expansion. "
|
||||
+ "Archetype coverage is a separate deterministic fixture; it does not claim every building occurs on every seed. "
|
||||
+ "Zombie checks inspect the effective spawn table, not a guaranteed daylight population. No personal world is opened.");
|
||||
return report;
|
||||
}
|
||||
|
||||
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-alpha14" + (reload ? "-reload" : "") + "-seed-" + level.getSeed() + ".json");
|
||||
}
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
Path 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(java.nio.charset.StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.FenceBlock;
|
||||
import net.minecraft.world.level.block.IronBarsBlock;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.WallBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
|
||||
/** Independent oracle: actual native neighbor-update rules, with datapack tags loaded by the test server. */
|
||||
public final class NativeConnections20 {
|
||||
private NativeConnections20() {}
|
||||
public record Stats(int panes, int fences, int walls, int stairs, int nativeOracles, int externalNeighbors,
|
||||
int crossChunkPaneLinks, int settledExternalStates, int unsettledExternalStates) {
|
||||
public Stats add(Stats other) { return new Stats(panes + other.panes, fences + other.fences, walls + other.walls,
|
||||
stairs + other.stairs, nativeOracles + other.nativeOracles, externalNeighbors + other.externalNeighbors,
|
||||
crossChunkPaneLinks + other.crossChunkPaneLinks, settledExternalStates + other.settledExternalStates,
|
||||
unsettledExternalStates + other.unsettledExternalStates); }
|
||||
public static Stats empty() { return new Stats(0, 0, 0, 0, 0, 0, 0, 0, 0); }
|
||||
}
|
||||
public static Stats verify(GameTestHelper helper, Map<BlockPos, BlockState> geometry, Set<BlockPos> edits) {
|
||||
var level = helper.getLevel(); int panes = 0, fences = 0, walls = 0, stairs = 0, oracles = 0, exterior = 0, cross = 0, settled = 0, unsettled = 0;
|
||||
for (var entry : geometry.entrySet()) {
|
||||
var pos = entry.getKey(); var wanted = entry.getValue(); if (edits.contains(pos)) continue;
|
||||
boolean pane = wanted.getBlock() instanceof IronBarsBlock, fence = wanted.getBlock() instanceof FenceBlock;
|
||||
boolean wall = wanted.getBlock() instanceof WallBlock, stair = wanted.getBlock() instanceof StairBlock;
|
||||
if (!pane && !fence && !wall && !stair) continue;
|
||||
if (pane) panes++; if (fence) fences++; if (wall) walls++; if (stair) stairs++;
|
||||
var actual = level.getBlockState(pos);
|
||||
helper.assertTrue(sameNonConnectionState(actual, wanted),
|
||||
"Material, orientation, half or waterlogging changed at " + pos + ": expected " + wanted + " got " + actual);
|
||||
boolean knownNeighborhood = true;
|
||||
for (var side : Direction.values()) {
|
||||
var at = pos.relative(side); var expectedNeighbor = geometry.get(at); var actualNeighbor = level.getBlockState(at);
|
||||
// A changed neighbor's shape can propagate a legitimate exterior connection, e.g. stacked walls.
|
||||
if (edits.contains(at) || (expectedNeighbor == null ? !actualNeighbor.isAir()
|
||||
: !expectedNeighbor.equals(actualNeighbor))) knownNeighborhood = false;
|
||||
}
|
||||
var updated = actual;
|
||||
for (var side : Direction.values()) {
|
||||
var at = pos.relative(side);
|
||||
updated = updated.updateShape(level, level, pos, side, at, level.getBlockState(at), RandomSource.create(20));
|
||||
}
|
||||
if (knownNeighborhood) {
|
||||
helper.assertTrue(actual.equals(wanted), "Native detailed state differs inside known geometry at " + pos
|
||||
+ ": expected " + wanted + " got " + actual);
|
||||
helper.assertTrue(updated.equals(actual), "Vanilla neighbor rules disagree with generation20 connections at " + pos
|
||||
+ ": saved " + actual + " native " + updated);
|
||||
oracles++;
|
||||
} else {
|
||||
exterior++;
|
||||
if (updated.equals(actual)) { settled++; oracles++; }
|
||||
else {
|
||||
// A fresh structure has not necessarily received an update from untouched surrounding rock.
|
||||
// Permit only its exact planned state until that update; a partially/wrongly changed state fails.
|
||||
helper.assertTrue(actual.equals(wanted) && sameNonConnectionState(updated, wanted),
|
||||
"Exterior connection is neither the untouched plan nor a vanilla fixed point at " + pos
|
||||
+ ": plan " + wanted + " saved " + actual + " native " + updated);
|
||||
unsettled++;
|
||||
}
|
||||
}
|
||||
if (pane) for (var side : new Direction[] {Direction.EAST, Direction.SOUTH}) {
|
||||
var at = pos.relative(side); var neighbor = level.getBlockState(at);
|
||||
if (neighbor.getBlock() instanceof IronBarsBlock && (pos.getX() >> 4 != at.getX() >> 4 || pos.getZ() >> 4 != at.getZ() >> 4)) {
|
||||
var property = side == Direction.EAST ? BlockStateProperties.EAST : BlockStateProperties.SOUTH;
|
||||
helper.assertTrue(actual.getValue(property), "Pane connection disappeared exactly at a native chunk boundary: " + pos);
|
||||
cross++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Stats(panes, fences, walls, stairs, oracles, exterior, cross, settled, unsettled);
|
||||
}
|
||||
/** Normalize only geometry-derived properties. Everything else, including fluids and facing, stays exact. */
|
||||
private static boolean sameNonConnectionState(BlockState actual, BlockState wanted) {
|
||||
if (!actual.is(wanted.getBlock())) return false;
|
||||
if (wanted.getBlock() instanceof IronBarsBlock || wanted.getBlock() instanceof FenceBlock) {
|
||||
for (var property : new net.minecraft.world.level.block.state.properties.BooleanProperty[] {
|
||||
BlockStateProperties.NORTH, BlockStateProperties.EAST, BlockStateProperties.SOUTH, BlockStateProperties.WEST})
|
||||
actual = actual.setValue(property, wanted.getValue(property));
|
||||
} else if (wanted.getBlock() instanceof WallBlock) {
|
||||
for (var property : WallBlock.PROPERTY_BY_DIRECTION.values()) actual = actual.setValue(property, wanted.getValue(property));
|
||||
actual = actual.setValue(WallBlock.UP, wanted.getValue(WallBlock.UP));
|
||||
} else if (wanted.getBlock() instanceof StairBlock) actual = actual.setValue(StairBlock.SHAPE, wanted.getValue(StairBlock.SHAPE));
|
||||
return actual.equals(wanted);
|
||||
}
|
||||
|
||||
}
|
||||
-1030
File diff suppressed because it is too large
Load Diff
-140
@@ -1,140 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationChecks;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationSampler;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
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.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
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.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
|
||||
final class PopulationDensityEquivalenceDiagnostics {
|
||||
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
|
||||
|
||||
static void verify(GameTestHelper helper) throws Exception {
|
||||
Map<String, Object> adversarial = ExactInterpolationChecks.verify();
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
var densities = registries.lookupOrThrow(Registries.DENSITY_FUNCTION);
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
List<Map<String, Object>> reports = new ArrayList<>();
|
||||
List<Pair> pairs = new ArrayList<>();
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
var key = SanctuaryMod.id("population_" + players);
|
||||
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
|
||||
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
|
||||
var density = (PopulationIslandDensity) registered;
|
||||
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
|
||||
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
|
||||
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
|
||||
density.detail(), density.underside(), density.riftWarp(), density.riftDetail(), density.capacity());
|
||||
for (long seed : new long[]{0, 42, 8675309}) {
|
||||
var state = RandomState.create(noises, seed,
|
||||
settings.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, key)).value());
|
||||
var pair = new Pair(players, seed, state.getSampler(reference), state.getSampler(density));
|
||||
pairs.add(pair);
|
||||
SamplerContext context = SamplerContext.builder().enableCaches().build();
|
||||
long comparisons = 0, digest = 0xcbf29ce484222325L;
|
||||
for (var volume : volumes(IslandCapacity.fromPlayers(players))) {
|
||||
DensityBuffer expected = DensityBuffer.createUnpooled(volume.size());
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.reference().sampleVolume(SamplerContext.EMPTY_UNCACHED, expected, volume);
|
||||
pair.actual().sampleVolume(context, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int index = volume.indexUnchecked(x, y, z);
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, expected.get(index), actual.get(index), wx, wy, wz, "bulk");
|
||||
same(pair, expected.get(index), pair.actual().sampleValue(context, wx, wy, wz), wx, wy, wz, "scalar after bulk");
|
||||
comparisons += 2;
|
||||
digest = (digest ^ Float.floatToRawIntBits(expected.get(index))) * 0x100000001b3L;
|
||||
}
|
||||
}
|
||||
Random order = new Random(seed ^ players);
|
||||
int limit = density.capacity().terrainLimit();
|
||||
for (int i = 0; i < 512; i++) {
|
||||
int x = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
int y = order.nextInt(400) - 8;
|
||||
int z = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
float expected = pair.reference().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z);
|
||||
same(pair, expected, pair.actual().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z), x, y, z, "uncached context");
|
||||
same(pair, expected, pair.actual().sampleValue(context, x, y, z), x, y, z, "context switch");
|
||||
comparisons += 2;
|
||||
}
|
||||
reports.add(Map.of("players", players, "seed", seed, "bitwise_comparisons", comparisons,
|
||||
"reference_density_digest", Long.toUnsignedString(digest, 16), "optimized_path_supported", true));
|
||||
SanctuaryMod.LOGGER.info("Exact density equivalence: players {}, seed {}, {} bitwise comparisons", players, seed, comparisons);
|
||||
}
|
||||
}
|
||||
long concurrentComparisons = 0;
|
||||
try (var executor = Executors.newFixedThreadPool(3)) {
|
||||
List<Callable<Long>> tasks = new ArrayList<>();
|
||||
for (int worker = 0; worker < 3; worker++) {
|
||||
int id = worker;
|
||||
tasks.add(() -> {
|
||||
long count = 0;
|
||||
SamplerContext local = SamplerContext.builder().enableCaches().build();
|
||||
// All workers share the same samplers and alternate worlds/seeds.
|
||||
// Only their scratch buffers and sampler contexts are thread-local.
|
||||
for (int round = 0; round < 3; round++) for (var pair : pairs) {
|
||||
var volume = new DensityVolume(9, 17, 7, -13 + id, 89 + round, -7 - id);
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.actual().sampleVolume(local, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, pair.reference().sampleValue(local, wx, wy, wz),
|
||||
actual.get(volume.indexUnchecked(x, y, z)), wx, wy, wz, "concurrent bulk");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
for (var result : executor.invokeAll(tasks)) concurrentComparisons += result.get();
|
||||
}
|
||||
Path report = Path.of("diagnostics", "population-alpha10-density-equivalence.json");
|
||||
Files.createDirectories(report.getParent());
|
||||
Files.writeString(report, new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
|
||||
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
|
||||
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
|
||||
}
|
||||
|
||||
private static List<DensityVolume> volumes(IslandCapacity capacity) {
|
||||
int limit = capacity.terrainLimit(), middle = (int) capacity.radius() / 2;
|
||||
return List.of(
|
||||
new DensityVolume(16, 384, 16, -8, 0, -8),
|
||||
new DensityVolume(19, 11, 17, -263, 61, -259),
|
||||
new DensityVolume(17, 17, 17, 249, 124, 249),
|
||||
new DensityVolume(13, 21, 11, 761, 167, -9),
|
||||
new DensityVolume(13, 21, 11, -775, 103, 503),
|
||||
new DensityVolume(17, 49, 5, limit - 12, 96, -2),
|
||||
new DensityVolume(17, 49, 5, -limit - 4, 96, -2),
|
||||
new DensityVolume(12, 33, 12, middle - 7, 90, -middle / 2 - 5),
|
||||
new DensityVolume(5, 13, 5, -2, -4, -2),
|
||||
new DensityVolume(5, 13, 5, -2, 378, -2),
|
||||
new DensityVolume(9, 13, 7, -257, 61, -9, 3, 5, 2));
|
||||
}
|
||||
|
||||
private static void same(Pair pair, float expected, float actual, int x, int y, int z, String phase) {
|
||||
if (Float.floatToRawIntBits(expected) != Float.floatToRawIntBits(actual))
|
||||
throw new AssertionError("Density changed from alpha10 in " + phase + ": players=" + pair.players() + ", seed=" + pair.seed()
|
||||
+ ", position=" + x + "," + y + "," + z + ", expected=" + Float.toHexString(expected) + ", actual=" + Float.toHexString(actual));
|
||||
}
|
||||
}
|
||||
-659
@@ -1,659 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
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.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.MultifaceBlock;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded engine probes at the centre AND in newly available terrain. No whole-island scan. */
|
||||
public final class PopulationDiagnostics {
|
||||
public static int regionsAtInitialSpawn;
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST};
|
||||
private PopulationDiagnostics() {}
|
||||
|
||||
static void verifyLegacyIsolation(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var binding = binding(level);
|
||||
var generator = binding.generator();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
helper.assertTrue(capacity != null && capacity.players() == (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified
|
||||
? unified.capacity().players() : Integer.getInteger("sanctuary.test.players", 10)),
|
||||
"The engine must create the requested production world preset");
|
||||
helper.assertFalse(HydrologyRuntime.enabled(generator) || NaturalHydrologyRuntime.enabled(generator)
|
||||
|| LayeredHydrologyRuntime.enabled(generator) || WoodlandHydrologyRuntime.enabled(generator)
|
||||
|| RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
|
||||
"Only the population hydrology pipeline may run with the new settings");
|
||||
var settings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
for (var key : List.of(SanctuarySpawn.SETTINGS, SanctuarySpawn.RAISED_SETTINGS, SanctuarySpawn.HYDROLOGY_SETTINGS,
|
||||
SanctuarySpawn.NATURAL_SETTINGS, SanctuarySpawn.LAYERED_SETTINGS, SanctuarySpawn.WOODLAND_SETTINGS,
|
||||
SanctuarySpawn.RIFT_SETTINGS, SanctuarySpawn.CAVERN_SETTINGS)) {
|
||||
var old = new NoiseBasedChunkGenerator(generator.getBiomeSource(), settings.getOrThrow(key));
|
||||
helper.assertFalse(PopulationHydrologyRuntime.enabled(old), "A saved settings key must stay isolated: " + key);
|
||||
helper.assertFalse(new PopulationGrovesFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||
"New trees must refuse older saved worlds");
|
||||
helper.assertFalse(new PopulationDecorationsFeature().place(level, old, RandomSource.create(0), BlockPos.ZERO),
|
||||
"New cave decorations must refuse older saved worlds");
|
||||
}
|
||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator) {
|
||||
helper.assertTrue(generator.getBiomeSource() instanceof SanctuaryBiomeSource
|
||||
&& generator.getBiomeSource().possibleBiomes().size() == (((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).source().citiesEnabled() ? 45 : 44)
|
||||
&& generator.getBiomeSource().possibleBiomes().stream().map(b -> b.getRegisteredName()).collect(java.util.stream.Collectors.toSet()).containsAll(SanctuaryBiomeSource.REQUIRED_BIOMES),
|
||||
"The unified generator retains all 44 historical biome holders, with Lost City added in city-enabled generations");
|
||||
helper.assertTrue(binding.random() != level.getChunkSource().randomState(),
|
||||
"The unified starter must use its real terrain state, never the expansion host fallback");
|
||||
} else {
|
||||
helper.assertTrue(generator.getBiomeSource() instanceof PopulationIslandBiomeSource,
|
||||
"The biome palette must use the new versioned resource family");
|
||||
helper.assertTrue(generator.getBiomeSource().possibleBiomes().size() == 10
|
||||
&& generator.getBiomeSource().possibleBiomes().stream().allMatch(b -> b.getRegisteredName().startsWith("sanctuary:population_")),
|
||||
"All ten layers of the ecology must remain available at each capacity");
|
||||
}
|
||||
}
|
||||
|
||||
static void verifyDensity(GameTestHelper helper) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var binding = binding(level);
|
||||
var generator = binding.generator();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
var density = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
|
||||
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
|
||||
var island = (PopulationIslandDensity) density;
|
||||
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
|
||||
var random = binding.random();
|
||||
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
|
||||
int step = Math.max(8, (int) Math.ceil(capacity.terrainLimit() / 32.0));
|
||||
int extent = (int) Math.ceil(capacity.terrainLimit() / (double) step) * step;
|
||||
int solidColumns = 0, outerColumns = 0, emptyColumns = 0;
|
||||
List<int[]> landCoordinates = new ArrayList<>();
|
||||
Set<Integer> bottomHeights = new TreeSet<>();
|
||||
StringBuilder map = new StringBuilder("x,z,bottom_y,top_y\n");
|
||||
for (int x = -extent; x <= extent; x += step) for (int z = -extent; z <= extent; z += step) {
|
||||
int bottom = -1, top = -1;
|
||||
for (int y = 8; y < 384; y += 8) {
|
||||
float value = random.sampleBlockValueUncached(density, x, y, z);
|
||||
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
|
||||
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
|
||||
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
|
||||
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
|
||||
if (x % (step * 8) == 0 && z % (step * 8) == 0)
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
|
||||
"Seed and capacity must replay identically without depending on region request order");
|
||||
}
|
||||
if (top >= 0) {
|
||||
solidColumns++;
|
||||
landCoordinates.add(new int[]{x, z});
|
||||
bottomHeights.add(bottom);
|
||||
if (Math.hypot(x, z) > IslandShape.TERRAIN_LIMIT + 16) outerColumns++;
|
||||
helper.assertTrue(Math.hypot(x, z) < capacity.terrainLimit(), "Terrain stays inside its finite declared boundary");
|
||||
} else emptyColumns++;
|
||||
map.append(x).append(',').append(z).append(',').append(bottom).append(',').append(top).append('\n');
|
||||
}
|
||||
helper.assertTrue(solidColumns > 100 && emptyColumns > 100 && bottomHeights.size() >= 4,
|
||||
"The sampled island must contain natural masses, holes and a varied underside");
|
||||
int additionalColumns = 0;
|
||||
if (capacity == IslandCapacity.TEN) {
|
||||
var small = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
for (int[] coordinate : landCoordinates) {
|
||||
boolean presentInSmall = false;
|
||||
for (int y = 8; y < 384 && !presentInSmall; y += 8)
|
||||
presentInSmall = random.sampleBlockValueUncached(small, coordinate[0], y, coordinate[1]) > 0;
|
||||
if (!presentInSmall) additionalColumns++;
|
||||
}
|
||||
helper.assertTrue(additionalColumns > 0,
|
||||
"Large must add real land columns compared with Small on the same seed and sample grid");
|
||||
} else helper.assertTrue(capacity.players() == 5 ? outerColumns == 0 : outerColumns > 100,
|
||||
"The historical large capacities must contain land beyond the old island boundary");
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
String prefix = "population-" + capacity.players() + "-seed-" + level.getSeed();
|
||||
Files.writeString(directory.resolve(prefix + "-density.csv"), map.toString());
|
||||
Files.writeString(directory.resolve(prefix + "-density.json"), json(Map.of("players", capacity.players(),
|
||||
"seed", level.getSeed(), "step", step, "solid_columns", solidColumns, "empty_columns", emptyColumns,
|
||||
"solid_columns_beyond_old_boundary", outerColumns, "underside_heights", bottomHeights,
|
||||
"scope", "sparse actual density samples; no resource extrapolation", "passed", true,
|
||||
"additional_columns_vs_small", capacity == IslandCapacity.TEN ? additionalColumns : -1)));
|
||||
SanctuaryMod.LOGGER.info("Capacity {} density seed {}: {} land columns, {} beyond the old envelope",
|
||||
capacity.players(), level.getSeed(), solidColumns, outerColumns);
|
||||
}
|
||||
|
||||
static void verifyExterior(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var capacity = IslandCapacity.forGenerator(binding(level).generator());
|
||||
int edge = Math.floorDiv(capacity.terrainLimit() + 48, 16) + 1;
|
||||
for (int[] point : new int[][]{{edge,0},{-edge-1,0},{0,edge},{0,-edge-1},{256,0},{-257,0}}) {
|
||||
var chunk = full(helper, level, point[0], point[1]);
|
||||
for (var section : chunk.getSections()) helper.assertTrue(section.hasOnlyAir(),
|
||||
"Completed terrain and decoration must remain void beyond this capacity: " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
|
||||
static void verifyLowerMargin(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
for (int[] point : new int[][]{{0,0},{-8,0},{8,0},{0,-8},{0,8}}) {
|
||||
var chunk = full(helper, level, point[0], point[1]);
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 0; y < 16; y++) {
|
||||
var pos = new BlockPos(point[0]*16+x, y, point[1]*16+z);
|
||||
var block = chunk.getBlockState(pos);
|
||||
helper.assertTrue(block.isAir() || block.getFluidState().is(FluidTags.WATER) || block.getFluidState().is(FluidTags.LAVA),
|
||||
"The increased horizontal size must not add a lower floor: " + pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void start(GameTestHelper helper) throws IOException {
|
||||
var context = new Context(helper);
|
||||
try { context.prepare(); }
|
||||
catch (RuntimeException | Error | IOException e) { context.cleanup(); throw e; }
|
||||
helper.onEachTick(() -> {
|
||||
try { context.tick(); }
|
||||
catch (RuntimeException | Error e) { context.cleanup(); throw e; }
|
||||
});
|
||||
}
|
||||
|
||||
private static final class Context {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final NoiseBasedChunkGenerator generator;
|
||||
final RandomState random;
|
||||
final IslandCapacity capacity;
|
||||
final List<PopulationHydrologyRuntime.RegionPlan> regions = new ArrayList<>();
|
||||
final Map<Long, LevelChunk> chunks = new TreeMap<>();
|
||||
final Set<Long> forced = new HashSet<>();
|
||||
final Set<BlockPos> expectedWater = new HashSet<>();
|
||||
final Map<BlockPos, PopulationHydrology.Cell> selectedCells = new LinkedHashMap<>();
|
||||
final Map<BlockPos, BlockState> naturalLogs = new HashMap<>();
|
||||
final Set<BlockPos> declaredWater = new HashSet<>();
|
||||
final Set<BlockPos> expectedLava = new HashSet<>();
|
||||
final Set<BlockPos> lavaSupports = new HashSet<>();
|
||||
final List<PopulationLavaDeposit.Cell> pocketCells = new ArrayList<>();
|
||||
final List<PopulationLavaFlows.Fall> lavaFalls = new ArrayList<>();
|
||||
final List<Map<String,Object>> flowReports = new ArrayList<>();
|
||||
int reentryWitnesses;
|
||||
final List<PopulationHydrology.Spring> springs = new ArrayList<>();
|
||||
final List<PopulationHydrology.Spill> spills = new ArrayList<>();
|
||||
final List<Map<String,Object>> regionReports = new ArrayList<>();
|
||||
long startTime;
|
||||
boolean running, closed;
|
||||
|
||||
Context(GameTestHelper helper) {
|
||||
this.helper = helper; level = helper.getLevel();
|
||||
var binding = binding(level);
|
||||
generator = binding.generator();
|
||||
random = binding.random();
|
||||
capacity = IslandCapacity.forGenerator(generator);
|
||||
}
|
||||
|
||||
void prepare() throws IOException {
|
||||
boolean heritage = level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.rootGeneration() == 22 || (sanctuary.rootGeneration() == 19 || sanctuary.rootGeneration() == 20 || sanctuary.rootGeneration() == 21) && sanctuary.experimentalStructures());
|
||||
// Generation19 explicitly prospects destinations across the root island before publishing them.
|
||||
// A consulted hydrology region is a bounded immutable plan, not a loaded/generated chunk.
|
||||
// Historical generators retain their original near-spawn contract.
|
||||
int regionLimit = 4;
|
||||
if (heritage) {
|
||||
int extent = capacity.terrainLimit();
|
||||
var first = capacity.regionAt(-extent, -extent); var last = capacity.regionAt(extent, extent);
|
||||
regionLimit = 0;
|
||||
for (int x = first.x(); x <= last.x(); x++) for (int z = first.z(); z <= last.z(); z++) {
|
||||
var region = new IslandCapacity.Region(x, z);
|
||||
double nearestX = Math.max(0, Math.max(region.minX(), -region.maxX()));
|
||||
double nearestZ = Math.max(0, Math.max(region.minZ(), -region.maxZ()));
|
||||
if (Math.hypot(nearestX, nearestZ) < extent) regionLimit++;
|
||||
}
|
||||
helper.assertTrue(regionLimit <= 9, "Heritage starter hydrology must fit within nine local regions");
|
||||
var plan = ((SanctuaryChunkGenerator) level.getChunkSource().getGenerator()).heritagePlan19();
|
||||
helper.assertTrue(plan.columns() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_COLUMNS
|
||||
&& plan.samples() <= fr.koka.sanctuary.worldgen.heritage.HeritagePlan19.MAX_SAMPLES,
|
||||
"Island-wide heritage prospecting exceeded its independent column/probe budget");
|
||||
}
|
||||
helper.assertTrue(regionsAtInitialSpawn > 0 && regionsAtInitialSpawn <= regionLimit,
|
||||
"Initial hydrology exceeded its geographic planning scope (limit " + regionLimit + "): " + regionsAtInitialSpawn);
|
||||
List<IslandCapacity.Region> targets = new ArrayList<>();
|
||||
targets.add(new IslandCapacity.Region(0,0));
|
||||
if (capacity.players() == 20) targets.add(new IslandCapacity.Region(1,0));
|
||||
if (capacity.players() == 100) {
|
||||
targets.add(new IslandCapacity.Region(1,1));
|
||||
targets.add(new IslandCapacity.Region(2,0));
|
||||
}
|
||||
Set<Long> requested = new TreeSet<>();
|
||||
for (var target : targets) {
|
||||
SanctuaryMod.LOGGER.info("Capacity {}: inspecting hydrology in region {}", capacity.players(), target);
|
||||
var region = PopulationHydrologyRuntime.regionPlan(generator, random, target);
|
||||
regions.add(region);
|
||||
var plan = region.water();
|
||||
checkOwnership(region);
|
||||
var ids = new HashSet<Long>();
|
||||
plan.features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.LAKE || f.kind() == PopulationHydrology.Kind.POND)
|
||||
.max(Comparator.comparingInt(PopulationHydrology.Feature::waterCells)).ifPresent(f -> ids.add(f.id()));
|
||||
if (!plan.terraces().isEmpty()) ids.addAll(plan.terraces().getFirst().basinIds());
|
||||
if (ids.isEmpty()) plan.features().stream().findFirst().ifPresent(f -> ids.add(f.id()));
|
||||
for (var cell : plan.cells()) if (ids.contains(cell.featureId())) {
|
||||
selectedCells.put(new BlockPos(cell.x(), cell.bedY(), cell.z()), cell);
|
||||
request(requested, cell.x() >> 4, cell.z() >> 4, 1);
|
||||
if (cell.hasWater()) for (int y = cell.bedY()+1; y <= cell.waterY(); y++) expectedWater.add(new BlockPos(cell.x(),y,cell.z()));
|
||||
}
|
||||
for (var spill : plan.spills()) if (ids.contains(spill.fromFeatureId())) {
|
||||
spills.add(spill); request(requested, spill.flowBounds());
|
||||
}
|
||||
if (!plan.springs().isEmpty()) {
|
||||
var spring = plan.springs().getFirst(); springs.add(spring);
|
||||
request(requested, spring.flowBounds()); expectedWater.add(pos(spring.source()));
|
||||
}
|
||||
// Inspect the finite pocket and at most one volcanic fall per selected region.
|
||||
var lava = region.lava();
|
||||
Set<BlockPos> allFallSources = new HashSet<>(), allFallSupports = new HashSet<>();
|
||||
for (var fall : lava.falls()) {
|
||||
allFallSources.add(lavaPos(fall.source()));
|
||||
fall.supports().forEach(p -> allFallSupports.add(lavaPos(p)));
|
||||
}
|
||||
for (var cell : lava.cells()) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.y(), cell.z());
|
||||
if (allFallSources.contains(p)) continue;
|
||||
pocketCells.add(cell);
|
||||
request(requested, cell.x() >> 4, cell.z() >> 4, 1);
|
||||
if (cell.lava()) expectedLava.add(p);
|
||||
}
|
||||
for (var support : lava.supports()) if (!allFallSupports.contains(lavaPos(support))) {
|
||||
lavaSupports.add(lavaPos(support));
|
||||
request(requested, support.x() >> 4, support.z() >> 4, 1);
|
||||
}
|
||||
if (!lava.falls().isEmpty()) {
|
||||
var fall = lava.falls().getFirst(); lavaFalls.add(fall);
|
||||
request(requested, fall.flowBounds()); expectedLava.add(lavaPos(fall.source()));
|
||||
fall.supports().forEach(p -> lavaSupports.add(lavaPos(p)));
|
||||
}
|
||||
// A small, independently selected patch proves full ecology beyond the former radius.
|
||||
int ox = target.originX(), oz = target.originZ();
|
||||
int bestX = ox, bestZ = oz; int highest = -1;
|
||||
for (int dx = -144; dx <= 144; dx += 48) for (int dz = -144; dz <= 144; dz += 48) {
|
||||
int x = ox+dx, z=oz+dz;
|
||||
int top = generator.getBaseHeight(x,z,net.minecraft.world.level.levelgen.Heightmap.Types.WORLD_SURFACE,
|
||||
level,random);
|
||||
if (top > highest) { highest=top; bestX=x; bestZ=z; }
|
||||
}
|
||||
helper.assertTrue(highest > 48, "Every inspected region must contain real natural terrain");
|
||||
request(requested, bestX >> 4, bestZ >> 4, 2);
|
||||
regionReports.add(new LinkedHashMap<>(Map.of("region_x",target.x(),"region_z",target.z(),
|
||||
"planned_features",plan.features().size(),"planned_cells",plan.cells().size(),
|
||||
"planned_springs",plan.springs().size(),"planned_terraces",plan.terraces().size(),
|
||||
"ecology_patch_x",bestX,"ecology_patch_z",bestZ)));
|
||||
}
|
||||
helper.assertTrue(!expectedWater.isEmpty(), "The test must inspect retained production water");
|
||||
if (capacity.players() > 10) helper.assertTrue(regions.stream().skip(1).anyMatch(r -> !r.water().cells().isEmpty()),
|
||||
"Hydrology must occur on newly available terrain, not only around the old central island");
|
||||
SanctuaryMod.LOGGER.info("Capacity {}: preparing {} bounded FULL chunks",capacity.players(),requested.size());
|
||||
for (long packed : requested) {
|
||||
var p=ChunkPos.unpack(packed);
|
||||
if (level.setChunkForced(p.x(),p.z(),true)) forced.add(packed);
|
||||
}
|
||||
for (long packed : requested) {
|
||||
var p=ChunkPos.unpack(packed); chunks.put(packed,full(helper,level,p.x(),p.z()));
|
||||
}
|
||||
for (long packed : requested) level.waitForEntities(ChunkPos.unpack(packed),0);
|
||||
declaredWater.addAll(expectedWater);
|
||||
captureNaturalLogs();
|
||||
captureEcology();
|
||||
export(false,0);
|
||||
}
|
||||
|
||||
void checkOwnership(PopulationHydrologyRuntime.RegionPlan region) {
|
||||
var owner=region.region(); var plan=region.water();
|
||||
Set<Long> ids=new HashSet<>();
|
||||
for(var feature:plan.features()) helper.assertTrue(ids.add(feature.id()),"Feature IDs must be unique within a region");
|
||||
for(var cell:plan.cells()) {
|
||||
helper.assertTrue(cell.x()>=owner.minX() && cell.x()<=owner.maxX() && cell.z()>=owner.minZ() && cell.z()<=owner.maxZ(),
|
||||
"Hydrology must never be truncated at or written across region ownership: "+cell);
|
||||
helper.assertTrue(ids.contains(cell.featureId()),"Each retained cell needs its complete owning feature");
|
||||
for(int y=cell.bedY()-cell.sedimentDepth()-1;y<=cell.bedY();y++)
|
||||
helper.assertTrue(random.sampleBlockValueUncached(
|
||||
generator.generatorSettings().value().noiseRouter().finalDensity(),cell.x(),y,cell.z())>0,
|
||||
"Regional translation must retain the real natural support below sediment: "+cell);
|
||||
}
|
||||
for(var terrace:plan.terraces()) helper.assertTrue(ids.containsAll(terrace.basinIds()),
|
||||
"Admission must retain or reject a complete group of water terraces");
|
||||
for(var spill:plan.spills()) helper.assertTrue(ids.contains(spill.fromFeatureId())
|
||||
&& (spill.toFeatureId()==-1 || ids.contains(spill.toFeatureId())),
|
||||
"No cascade may refer to a basin removed by region admission");
|
||||
for(var bounds:region.lava().flowBounds()) helper.assertTrue(bounds.minX()>=owner.minX() && bounds.maxX()<=owner.maxX()
|
||||
&& bounds.minZ()>=owner.minZ() && bounds.maxZ()<=owner.maxZ(),"Volcanic flows must respect the same ownership");
|
||||
}
|
||||
|
||||
void captureEcology() {
|
||||
for (int i=0;i<regions.size();i++) {
|
||||
var region=regions.get(i).region(); var counts=new TreeMap<String,Long>();
|
||||
long logs=0,ores=0,nonAir=0;
|
||||
for(var chunk:chunks.values()) {
|
||||
int x=chunk.getPos().getMinBlockX(),z=chunk.getPos().getMinBlockZ();
|
||||
if (!capacity.regionAt(x,z).equals(region)) continue;
|
||||
for(var section:chunk.getSections()) section.getStates().count((state,count)->{
|
||||
if(!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(),(long)count,Long::sum);
|
||||
});
|
||||
}
|
||||
for(var entry:counts.entrySet()) {
|
||||
nonAir+=entry.getValue();
|
||||
if(entry.getKey().endsWith("_log")) logs+=entry.getValue();
|
||||
if(entry.getKey().endsWith("_ore")) ores+=entry.getValue();
|
||||
}
|
||||
helper.assertTrue(nonAir>1000,"Distant FULL chunks must contain the new island");
|
||||
helper.assertTrue(logs>0,"Trees must decorate each inspected region, including outside the old envelope");
|
||||
helper.assertTrue(ores>0,"Ore features must also decorate the new distant terrain");
|
||||
regionReports.get(i).put("observed_blocks",counts);
|
||||
regionReports.get(i).put("observed_logs",logs);
|
||||
regionReports.get(i).put("observed_ores",ores);
|
||||
}
|
||||
}
|
||||
|
||||
void tick() {
|
||||
if(closed || running || helper.getTick()<20) return;
|
||||
if(!chunks.keySet().stream().allMatch(level::isPositionTickingWithEntitiesLoaded)) {
|
||||
helper.assertTrue(helper.getTick()<800,"All forced inspection chunks must become ready for real fluid ticks");
|
||||
return;
|
||||
}
|
||||
running=true; startTime=level.getGameTime();
|
||||
verifyWater();
|
||||
verifyFinishedShell();
|
||||
verifyLava();
|
||||
helper.runAfterDelay(1800,()->{
|
||||
try {
|
||||
helper.assertTrue(chunks.keySet().stream().allMatch(level::isPositionTickingWithEntitiesLoaded),
|
||||
"Chunks must remain scheduler-ready throughout the measured fluid interval");
|
||||
verifyWater();
|
||||
verifyFinishedShell();
|
||||
verifyLava();
|
||||
for (var spring : springs) verifySpring(spring);
|
||||
for(var spill:spills) {
|
||||
Set<BlockPos> destination=null;
|
||||
if(spill.toFeatureId()!=-1) {
|
||||
destination=new HashSet<>();
|
||||
for(var region:regions) for(var cell:region.water().cells()) if(cell.featureId()==spill.toFeatureId() && cell.hasWater())
|
||||
destination.add(new BlockPos(cell.x(),cell.waterY(),cell.z()));
|
||||
}
|
||||
int drop=connectedDescent(pos(spill.source()),pos(spill.outlet()),destination);
|
||||
helper.assertTrue(drop >= (destination==null?12:4),"Each retained cascade must connect downhill to its basin or fall: "+spill);
|
||||
}
|
||||
for (var fall : lavaFalls) {
|
||||
int descent = connectedDescent(lavaPos(fall.source()), lavaPos(fall.outlet()), null, true);
|
||||
helper.assertTrue(descent >= 8, "A translated volcanic fall must descend through real lava ticks: " + fall.source());
|
||||
}
|
||||
verifyPostProcessingReentry();
|
||||
export(true,level.getGameTime()-startTime);
|
||||
helper.succeed();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
try { export(false, level.getGameTime() - startTime); }
|
||||
catch (IOException e) { failure.addSuppressed(e); }
|
||||
throw failure;
|
||||
} catch(IOException e) {throw new IllegalStateException(e);}
|
||||
finally {cleanup();}
|
||||
});
|
||||
}
|
||||
|
||||
PopulationHydrology.Plan waterPlanAt(int x, int z) {
|
||||
var owner = capacity.regionAt(x, z);
|
||||
return regions.stream().filter(r -> r.region().equals(owner)).map(PopulationHydrologyRuntime.RegionPlan::water)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
PopulationHydrology.Cell cellAt(BlockPos p) {
|
||||
var plan = waterPlanAt(p.getX(), p.getZ());
|
||||
return plan == null ? null : plan.cellAt(p.getX(), p.getY(), p.getZ());
|
||||
}
|
||||
|
||||
float natural(BlockPos p) {
|
||||
return random.sampleBlockValueUncached(
|
||||
generator.generatorSettings().value().noiseRouter().finalDensity(), p.getX(), p.getY(), p.getZ());
|
||||
}
|
||||
|
||||
void captureNaturalLogs() {
|
||||
for (BlockPos p : declaredWater) {
|
||||
BlockState state = level.getBlockState(p);
|
||||
if (state.getFluidState().is(FluidTags.WATER) && state.getFluidState().isSource()) continue;
|
||||
var cell = cellAt(p);
|
||||
boolean horizontal = state.is(BlockTags.LOGS) && state.hasProperty(BlockStateProperties.AXIS)
|
||||
&& state.getValue(BlockStateProperties.AXIS) != Direction.Axis.Y;
|
||||
boolean lowerWater = declaredWater.contains(p.below())
|
||||
&& level.getFluidState(p.below()).is(FluidTags.WATER) && level.getFluidState(p.below()).isSource();
|
||||
boolean bed = cell != null && cell.bedY() == p.getY() - 1
|
||||
&& level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below());
|
||||
boolean beside = Arrays.stream(HORIZONTAL).map(p::relative).anyMatch(side -> declaredWater.contains(side)
|
||||
&& level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource());
|
||||
helper.assertTrue(cell != null && horizontal && p.getY() == cell.waterY() && (lowerWater || bed) && beside,
|
||||
"Only a supported horizontal surface log beside real retained source water may occupy a planned water voxel: " + p + "=" + state);
|
||||
naturalLogs.put(p.immutable(), state); expectedWater.remove(p);
|
||||
}
|
||||
helper.assertTrue(expectedWater.size() + naturalLogs.size() == declaredWater.size(),
|
||||
"Every selected water voxel must have an exact retained source or recorded log");
|
||||
}
|
||||
|
||||
void verifyFinishedShell() {
|
||||
for (var cell : selectedCells.values()) {
|
||||
var plan = waterPlanAt(cell.x(), cell.z());
|
||||
var kind = plan.features().stream().filter(f -> f.id() == cell.featureId()).findFirst().orElseThrow().kind();
|
||||
int cut = !cell.hasWater() ? kind == PopulationHydrology.Kind.RIVER ? PopulationHydrology.MAX_RIVER_BANK_CARVE : 2
|
||||
: kind == PopulationHydrology.Kind.TERRACE ? PopulationTerraces.MAX_CARVE
|
||||
: kind == PopulationHydrology.Kind.LAKE ? PopulationHydrology.MAX_LAKE_CARVE : PopulationHydrology.MAX_CARVE;
|
||||
helper.assertTrue(cell.sedimentDepth() >= 3 && cell.sedimentDepth() <= 5 && cell.carveTop() - cell.bedY() <= cut,
|
||||
"Regional sediment and carving must keep their feature-specific budgets: " + cell);
|
||||
for (int d = 0; d <= cell.sedimentDepth() + 1; d++) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.bedY() - d, cell.z());
|
||||
BlockState actual = level.getBlockState(p);
|
||||
helper.assertTrue(natural(p) > 0 && actual.isCollisionShapeFullBlock(level, p),
|
||||
"Every sediment layer and its two natural supports must remain full after generation and ticks: " + p + "=" + actual);
|
||||
if (d >= cell.sedimentDepth()) continue;
|
||||
BlockState expected = PopulationMaterials.sedimentBlock(cell, random.seed(), d);
|
||||
boolean ore = actual.is(Blocks.COAL_ORE) || actual.is(Blocks.DEEPSLATE_COAL_ORE)
|
||||
|| actual.is(Blocks.IRON_ORE) || actual.is(Blocks.DEEPSLATE_IRON_ORE)
|
||||
|| actual.is(Blocks.COPPER_ORE) || actual.is(Blocks.DEEPSLATE_COPPER_ORE)
|
||||
|| actual.is(Blocks.GOLD_ORE) || actual.is(Blocks.DEEPSLATE_GOLD_ORE)
|
||||
|| actual.is(Blocks.REDSTONE_ORE) || actual.is(Blocks.DEEPSLATE_REDSTONE_ORE)
|
||||
|| actual.is(Blocks.LAPIS_ORE) || actual.is(Blocks.DEEPSLATE_LAPIS_ORE)
|
||||
|| actual.is(Blocks.DIAMOND_ORE) || actual.is(Blocks.DEEPSLATE_DIAMOND_ORE)
|
||||
|| actual.is(Blocks.EMERALD_ORE) || actual.is(Blocks.DEEPSLATE_EMERALD_ORE);
|
||||
helper.assertTrue(actual.equals(expected) || expected.is(BlockTags.BASE_STONE_OVERWORLD) && ore
|
||||
|| expected.is(Blocks.GRASS_BLOCK) && actual.is(Blocks.DIRT),
|
||||
"The selected regional sediment profile must survive decoration: " + p + " expected=" + expected + " actual=" + actual);
|
||||
}
|
||||
if (!cell.hasWater()) continue;
|
||||
int maxDepth = kind == PopulationHydrology.Kind.TERRACE ? PopulationTerraces.MAX_DEPTH : PopulationHydrology.MAX_DEPTH;
|
||||
helper.assertTrue(cell.waterY() > cell.bedY() && cell.waterY() - cell.bedY() <= maxDepth,
|
||||
"Retained water must respect its feature depth limit");
|
||||
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) for (Direction direction : HORIZONTAL) {
|
||||
BlockPos wall = new BlockPos(cell.x(), y, cell.z()).relative(direction);
|
||||
var next = cellAt(wall);
|
||||
boolean plannedWater = next != null && next.hasWater() && y > next.bedY() && y <= next.waterY();
|
||||
if (plannedWater || plan.isSpillOpening(wall.getX(), wall.getY(), wall.getZ())) continue;
|
||||
helper.assertTrue(natural(wall) > 0 && level.getBlockState(wall).isCollisionShapeFullBlock(level, wall),
|
||||
"A retained basin must preserve its actual natural underwater shell, except its declared outlet: " + wall);
|
||||
}
|
||||
if (kind == PopulationHydrology.Kind.TERRACE) {
|
||||
for (int y = cell.carveTop() + 1; y <= cell.carveTop() + 4; y++)
|
||||
helper.assertTrue(natural(new BlockPos(cell.x(), y, cell.z())) <= 0, "A cave terrace needs four natural headroom blocks");
|
||||
boolean roof = false;
|
||||
for (int y = cell.carveTop() + 5; y < 384 && !roof; y++) roof = natural(new BlockPos(cell.x(), y, cell.z())) > 0;
|
||||
helper.assertTrue(roof, "A lower terrace must remain below an existing natural roof");
|
||||
} else for (int y = cell.carveTop() + 1; y < 384; y++)
|
||||
helper.assertTrue(natural(new BlockPos(cell.x(), y, cell.z())) <= 0, "A surface basin must be exposed to the natural sky");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyLava() {
|
||||
for (BlockPos p : expectedLava) helper.assertTrue(natural(p) > 0
|
||||
&& level.getFluidState(p).is(FluidTags.LAVA) && level.getFluidState(p).isSource(),
|
||||
"Every inspected lava source must replace natural rock and survive actual fluid ticks: " + p);
|
||||
for (BlockPos p : lavaSupports) {
|
||||
BlockState block = level.getBlockState(p);
|
||||
helper.assertTrue(natural(p) > 0 && block.isCollisionShapeFullBlock(level, p) && !block.ignitedByLava(),
|
||||
"A selected volcanic niche must keep its actual nonflammable natural support: " + p);
|
||||
}
|
||||
for (var cell : pocketCells) if (!cell.lava()) {
|
||||
BlockPos p = new BlockPos(cell.x(), cell.y(), cell.z());
|
||||
BlockState block = level.getBlockState(p);
|
||||
// Wall lichen can light the excavated chamber without obstructing its dry access.
|
||||
helper.assertTrue(natural(p) > 0 && (block.isAir() || block.is(Blocks.GLOW_LICHEN))
|
||||
&& block.getCollisionShape(level, p).isEmpty() && block.getFluidState().isEmpty(),
|
||||
"The lava pocket must keep its excavated two-block approach and chamber accessible: " + p + " = " + block);
|
||||
}
|
||||
}
|
||||
|
||||
void verifySpring(PopulationHydrology.Spring spring) {
|
||||
var plan = waterPlanAt(spring.outlet().x(), spring.outlet().z());
|
||||
var channel = plan == null ? null : plan.cellAt(spring.outlet().x(), spring.outlet().z());
|
||||
if (channel != null && plan.isRiverCell(channel) && spring.outlet().y() > channel.waterY()
|
||||
&& spring.outlet().y() <= channel.carveTop()) {
|
||||
int expectedDrop = spring.source().y() - channel.waterY();
|
||||
helper.assertTrue(expectedDrop >= 5, "A river inlet must start above its retained channel");
|
||||
Set<BlockPos> destination = new HashSet<>();
|
||||
for (var cell : plan.cells()) if (cell.featureId() == channel.featureId() && cell.hasWater())
|
||||
destination.add(new BlockPos(cell.x(), cell.waterY(), cell.z()));
|
||||
helper.assertTrue(connectedDescent(pos(spring.source()), pos(spring.outlet()), destination) >= expectedDrop,
|
||||
"The translated inlet must actually connect downhill into its river");
|
||||
} else {
|
||||
int predicted = spring.source().y() - spring.flowPath().stream().mapToInt(PopulationHydrology.Position::y).min().orElse(spring.source().y());
|
||||
helper.assertTrue(connectedDescent(pos(spring.source()), pos(spring.outlet()), null) >= Math.max(8, Math.min(16, predicted)),
|
||||
"An independent spring must actually descend through vanilla ticks: " + spring.source());
|
||||
}
|
||||
}
|
||||
|
||||
void verifyPostProcessingReentry() {
|
||||
var outlets = new LinkedHashSet<BlockPos>();
|
||||
if (!springs.isEmpty()) outlets.add(pos(springs.getFirst().outlet()));
|
||||
if (!spills.isEmpty()) outlets.add(pos(spills.getFirst().outlet()));
|
||||
if (!lavaFalls.isEmpty()) outlets.add(lavaPos(lavaFalls.getFirst().outlet()));
|
||||
for (BlockPos outlet : outlets) {
|
||||
LevelChunk chunk = chunks.get(ChunkPos.pack(outlet));
|
||||
helper.assertTrue(chunk != null && Arrays.stream(chunk.getPostProcessing()).allMatch(m -> m == null || m.isEmpty()),
|
||||
"Re-entry uses a finished inspected chunk with consumed generation markers");
|
||||
helper.assertTrue(PopulationSpringOutlets.capturePending(level, chunk).isEmpty(),
|
||||
"An already generated chunk has no pending population outlet work");
|
||||
BlockState original = level.getBlockState(outlet);
|
||||
BlockState witness = Blocks.GLOW_LICHEN.defaultBlockState().setValue(MultifaceBlock.getFaceProperty(Direction.UP), true);
|
||||
try {
|
||||
level.setBlock(outlet, witness, 2);
|
||||
chunk.postProcessGeneration(level);
|
||||
helper.assertTrue(level.getBlockState(outlet).equals(witness),
|
||||
"Post-processing re-entry must preserve a later construction at a completed outlet: " + outlet);
|
||||
} finally { level.setBlock(outlet, original, 2); }
|
||||
helper.assertTrue(level.getBlockState(outlet).equals(original), "Restore the development-world witness exactly");
|
||||
reentryWitnesses++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isFluid(BlockPos p, boolean lava) {
|
||||
return lava ? level.getFluidState(p).is(FluidTags.LAVA) : level.getFluidState(p).is(FluidTags.WATER);
|
||||
}
|
||||
|
||||
void verifyWater() {
|
||||
for (var entry : naturalLogs.entrySet()) {
|
||||
BlockPos p = entry.getKey();
|
||||
helper.assertTrue(level.getBlockState(p).equals(entry.getValue()), "A recorded natural log must retain its exact state");
|
||||
helper.assertTrue(level.getBlockState(p.below()).isCollisionShapeFullBlock(level, p.below())
|
||||
|| level.getFluidState(p.below()).is(FluidTags.WATER) && level.getFluidState(p.below()).isSource(),
|
||||
"A horizontal log must retain its original bed or source water underneath");
|
||||
helper.assertTrue(Arrays.stream(HORIZONTAL).map(p::relative).anyMatch(side -> expectedWater.contains(side)
|
||||
&& level.getFluidState(side).is(FluidTags.WATER) && level.getFluidState(side).isSource()),
|
||||
"A horizontal log must remain beside actual retained water");
|
||||
}
|
||||
for(var p:expectedWater) helper.assertTrue(level.getFluidState(p).is(FluidTags.WATER)
|
||||
&& level.getFluidState(p).isSource(),"Retained water must survive generation and vanilla fluid ticks: "+p+"="+level.getBlockState(p));
|
||||
}
|
||||
|
||||
int connectedDescent(BlockPos source,BlockPos outlet,Set<BlockPos> destination) {
|
||||
return connectedDescent(source, outlet, destination, false);
|
||||
}
|
||||
|
||||
int connectedDescent(BlockPos source,BlockPos outlet,Set<BlockPos> destination, boolean lava) {
|
||||
helper.assertTrue(source.distManhattan(outlet)==1,"A natural outlet must adjoin its source");
|
||||
helper.assertTrue(isFluid(outlet, lava),"The production post-processing must leave a flowing outlet: "+outlet);
|
||||
var seen=new HashSet<BlockPos>(); var queue=new ArrayDeque<BlockPos>();
|
||||
seen.add(outlet);queue.add(outlet);int drop=0;boolean reached=destination==null;
|
||||
while(!queue.isEmpty() && seen.size()<150000) {
|
||||
var at=queue.removeFirst(); drop=Math.max(drop,source.getY()-at.getY());
|
||||
if(destination!=null && destination.contains(at)) reached=true;
|
||||
for(var direction:new Direction[]{Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST}) {
|
||||
var next=at.relative(direction);
|
||||
if(next.getY()<0 || !chunks.containsKey(ChunkPos.pack(next)) || seen.contains(next))continue;
|
||||
if(isFluid(next, lava)) {seen.add(next);queue.addLast(next);}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(queue.isEmpty(), "The finite flow inspection must exhaust its search budget");
|
||||
helper.assertTrue(reached,"The actual downhill water component must reach its declared receiving basin");
|
||||
flowReports.add(Map.of("source", source, "outlet", outlet, "fluid", lava ? "lava" : "water",
|
||||
"actual_descending_blocks", seen.size(), "actual_drop", drop, "reaches_destination", reached));
|
||||
return drop;
|
||||
}
|
||||
|
||||
void export(boolean passed,long elapsed) throws IOException {
|
||||
Path output=Path.of("diagnostics","population-"+capacity.players()+"-seed-"+level.getSeed()+"-engine.json");
|
||||
Files.createDirectories(output.getParent());
|
||||
var report=new LinkedHashMap<String,Object>();
|
||||
report.put("players",capacity.players());report.put("seed",level.getSeed());
|
||||
report.put("terrain_seed", random.seed());
|
||||
if (level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator unified) {
|
||||
report.put("generation", 13); report.put("diameter", unified.initialDiameter());
|
||||
report.put("rejected_decoration_writes", unified.rejectedDecorationWrites());
|
||||
}
|
||||
report.put("mod_version",FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().getMetadata().getVersion().getFriendlyString());
|
||||
report.put("settings",generator.generatorSettings().getRegisteredName());
|
||||
report.put("regions_at_initial_spawn",regionsAtInitialSpawn);
|
||||
report.put("regions_cached_after_inspection",PopulationHydrologyRuntime.cachedRegionCount(random));
|
||||
report.put("regions",regionReports);report.put("inspected_full_chunks",chunks.size());
|
||||
report.put("retained_water_blocks",expectedWater.size());report.put("tested_springs",springs.size());
|
||||
report.put("tested_spills",spills.size());report.put("fluid_ticks",elapsed);
|
||||
report.put("selected_sediment_columns", selectedCells.size());
|
||||
report.put("natural_horizontal_logs", naturalLogs.size());
|
||||
report.put("retained_lava_sources", expectedLava.size()); report.put("tested_lava_falls", lavaFalls.size());
|
||||
report.put("flows", flowReports); report.put("postprocessing_reentry_witnesses", reentryWitnesses);
|
||||
report.put("scope","Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test");
|
||||
report.put("passed",passed);Files.writeString(output,json(report));
|
||||
if(passed) SanctuaryMod.LOGGER.info("Capacity {} seed {} passed: {} FULL chunks, {} retained water blocks, {} fluid ticks",
|
||||
capacity.players(),level.getSeed(),chunks.size(),expectedWater.size(),elapsed);
|
||||
}
|
||||
void cleanup() {
|
||||
if(closed)return;closed=true;
|
||||
for(long key:forced) {var p=ChunkPos.unpack(key);level.setChunkForced(p.x(),p.z(),false);}
|
||||
}
|
||||
}
|
||||
|
||||
private static PopulationGenerationContext.Binding binding(ServerLevel level) {
|
||||
return Objects.requireNonNull(PopulationGenerationContext.of(level), "This diagnostic requires Population or unified Sanctuary generation");
|
||||
}
|
||||
|
||||
private static String json(Object value) {return new GsonBuilder().setPrettyPrinting().create().toJson(value)+"\n";}
|
||||
private static BlockPos pos(PopulationHydrology.Position p) {return new BlockPos(p.x(),p.y(),p.z());}
|
||||
private static BlockPos lavaPos(PopulationLavaDeposit.Position p) {return new BlockPos(p.x(),p.y(),p.z());}
|
||||
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,"The inspected chunk must be fully generated");return(LevelChunk)chunk;
|
||||
}
|
||||
private static void request(Set<Long> requested,int x,int z,int halo) {
|
||||
for(int dx=-halo;dx<=halo;dx++)for(int dz=-halo;dz<=halo;dz++)requested.add(ChunkPos.pack(x+dx,z+dz));
|
||||
}
|
||||
private static void request(Set<Long> requested,PopulationHydrology.FlowBounds b) {
|
||||
for(int x=(b.minX()>>4)-1;x<=(b.maxX()>>4)+1;x++)for(int z=(b.minZ()>>4)-1;z<=(b.maxZ()>>4)+1;z++)requested.add(ChunkPos.pack(x,z));
|
||||
}
|
||||
}
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
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.resources.RegistryOps;
|
||||
import net.minecraft.tags.WorldPresetTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.WorldDimensions;
|
||||
import net.minecraft.world.level.levelgen.WorldGenSettings;
|
||||
import net.minecraft.world.level.levelgen.WorldOptions;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
|
||||
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 java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Uses the real 26.3 saved-data writer and reader; never opens the user's saves. */
|
||||
final class PopulationPersistenceDiagnostics {
|
||||
static void verify(GameTestHelper helper) throws IOException {
|
||||
var level = helper.getLevel();
|
||||
var access = level.registryAccess();
|
||||
var presets = access.lookupOrThrow(Registries.WORLD_PRESET);
|
||||
var standard = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id("sanctuary"))).value();
|
||||
helper.assertTrue(standard.createWorldDimensions().overworld() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator unified && unified.initialDiameter() == 724,
|
||||
"The public Sanctuary preset defaults to the unified medium geometry");
|
||||
var visible = presets.getOrThrow(WorldPresetTags.NORMAL).stream().map(holder -> holder.getRegisteredName())
|
||||
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
|
||||
helper.assertTrue(visible.equals(Set.of("sanctuary:sanctuary")),
|
||||
"Only Sanctuary is public; saved historical presets remain registered");
|
||||
helper.assertTrue(presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab")))
|
||||
.value().createWorldDimensions().overworld() instanceof fr.koka.sanctuary.expansion.ExpansionChunkGenerator,
|
||||
"The laboratory must use its own codec, without changing the existing presets");
|
||||
var oldDensity = (PopulationIslandDensity) access.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_20"))).value();
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var serialized = PopulationIslandDensity.CODEC.codec().encodeStart(ops, oldDensity).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("players");
|
||||
helper.assertTrue(PopulationIslandDensity.CODEC.codec().parse(ops, serialized).getOrThrow().capacity() == IslandCapacity.TWENTY,
|
||||
"An omitted alpha10 density field must keep its historical default, independently of the new world selector");
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
Path testRoot = Path.of("diagnostics", "population-persistence-seed-" + level.getSeed()).toAbsolutePath();
|
||||
Files.createDirectories(testRoot);
|
||||
LevelStorageSource storage = LevelStorageSource.createDefault(testRoot);
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
String presetId = "sanctuary_" + players;
|
||||
WorldDimensions dimensions = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id(presetId))).value().createWorldDimensions();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(dimensions.overworld()) == IslandCapacity.fromPlayers(players),
|
||||
"The selectable world type must resolve its declared capacity");
|
||||
var original = new WorldGenSettings(new WorldOptions(level.getSeed(), true, false), dimensions);
|
||||
String name = "players-" + players;
|
||||
try (var save = storage.createAccess(name)) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
var generator = (NoiseBasedChunkGenerator) restored.dimensions().overworld();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(generator) == IslandCapacity.fromPlayers(players),
|
||||
"Saved generator settings must preserve the chosen capacity after reading the world from disk");
|
||||
helper.assertTrue(generator.generatorSettings().getRegisteredName().equals("sanctuary:population_" + players),
|
||||
"The saved world must retain its explicit versioned settings key");
|
||||
helper.assertTrue(restored.options().seed() == level.getSeed(), "The seed must survive storage unchanged");
|
||||
results.add(Map.of("players", players, "preset", "sanctuary:" + presetId,
|
||||
"restored_settings", generator.generatorSettings().getRegisteredName(), "disk_round_trip", true));
|
||||
}
|
||||
}
|
||||
var old = new NoiseBasedChunkGenerator(standard.createWorldDimensions().overworld().getBiomeSource(),
|
||||
access.lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(SanctuarySpawn.CAVERN_SETTINGS));
|
||||
var savedOld = new WorldGenSettings(new WorldOptions(level.getSeed(), true, false),
|
||||
standard.createWorldDimensions().replaceOverworldGenerator(access, old));
|
||||
try (var save = storage.createAccess("legacy-alpha9")) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath("legacy-alpha9"), savedOld);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
var generator = (NoiseBasedChunkGenerator) restored.dimensions().overworld();
|
||||
helper.assertTrue(generator.generatorSettings().is(SanctuarySpawn.CAVERN_SETTINGS)
|
||||
&& IslandCapacity.forGenerator(generator) == null,
|
||||
"An existing alpha.9 settings reference must not acquire the new default capacity");
|
||||
}
|
||||
Files.writeString(testRoot.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"seed", level.getSeed(), "default_players", 10, "profiles", results,
|
||||
"legacy_settings_preserved", "sanctuary:sanctuary_cavern", "visible_presets", visible,
|
||||
"omitted_density_capacity_preserved", 20, "passed", true)) + "\n");
|
||||
}
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Exact post-generation observations in the disposable GameTest world; never edits terrain. */
|
||||
public final class ResourceSurveyDiagnostics {
|
||||
private static final Gson JSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
private static final int DECORATION_MARGIN = 16;
|
||||
private static final int PREPARATION_HALO_CHUNKS = 1;
|
||||
|
||||
private ResourceSurveyDiagnostics() {}
|
||||
|
||||
public static void verifyAndExport(GameTestHelper helper, LevelData.RespawnData initialSpawn) throws IOException {
|
||||
String mode = System.getProperty("sanctuary.test.resourceSurvey", "sample");
|
||||
if (mode.equals("off")) return;
|
||||
if (!mode.equals("sample") && !mode.equals("island")) {
|
||||
throw new IllegalArgumentException("sanctuary.test.resourceSurvey must be sample, island or off");
|
||||
}
|
||||
ServerLevel level = helper.getLevel();
|
||||
helper.assertTrue(SanctuarySpawn.usesSanctuaryGenerator(level),
|
||||
"A resource survey must inspect the production Sanctuary generator");
|
||||
helper.assertTrue(initialSpawn != null, "Use the actual initial spawn, not the remote GameTest fixture");
|
||||
Instant started = Instant.now();
|
||||
long startedNanos = System.nanoTime();
|
||||
long initialGameTime = level.getGameTime();
|
||||
int centreX = initialSpawn.pos().getX() >> 4;
|
||||
int centreZ = initialSpawn.pos().getZ() >> 4;
|
||||
IslandCapacity capacity = IslandCapacity.forGenerator(level.getChunkSource().getGenerator());
|
||||
int terrainRadius = capacity == null ? IslandShape.TERRAIN_LIMIT : capacity.terrainLimit();
|
||||
int envelopeRadius = terrainRadius + DECORATION_MARGIN;
|
||||
int minimumIslandChunk = Math.floorDiv(-envelopeRadius, 16);
|
||||
int maximumIslandChunk = Math.floorDiv(envelopeRadius - 1, 16);
|
||||
int minimumX = mode.equals("island") ? minimumIslandChunk : centreX - 1;
|
||||
int maximumX = mode.equals("island") ? maximumIslandChunk : centreX + 1;
|
||||
int minimumZ = mode.equals("island") ? minimumIslandChunk : centreZ - 1;
|
||||
int maximumZ = mode.equals("island") ? maximumIslandChunk : centreZ + 1;
|
||||
int expectedChunks = (maximumX - minimumX + 1) * (maximumZ - minimumZ + 1);
|
||||
BlockPos fixture = helper.absolutePos(BlockPos.ZERO);
|
||||
helper.assertTrue(fixture.getX() < minimumX * 16 - 1024 || fixture.getX() > (maximumX + 1) * 16 + 1024
|
||||
|| fixture.getZ() < minimumZ * 16 - 1024 || fixture.getZ() > (maximumZ + 1) * 16 + 1024,
|
||||
"The remote GameTest fixture must remain well outside the resource survey envelope");
|
||||
|
||||
// Complete the entire requested region, plus its immediate decoration neighbours, before
|
||||
// taking counts. Counting while still generating the next chunk would risk stale borders.
|
||||
SanctuaryMod.LOGGER.info("Resource survey {}: preparing {} inspected chunks plus a {}-chunk halo (seed {})",
|
||||
mode, expectedChunks, PREPARATION_HALO_CHUNKS, level.getSeed());
|
||||
int preparedChunks = 0;
|
||||
for (int x = minimumX - PREPARATION_HALO_CHUNKS; x <= maximumX + PREPARATION_HALO_CHUNKS; x++) {
|
||||
for (int z = minimumZ - PREPARATION_HALO_CHUNKS; z <= maximumZ + PREPARATION_HALO_CHUNKS; z++) {
|
||||
requireFullChunk(helper, level, x, z);
|
||||
preparedChunks++;
|
||||
}
|
||||
if (mode.equals("island") && (x - minimumX + PREPARATION_HALO_CHUNKS + 1) % 4 == 0) {
|
||||
SanctuaryMod.LOGGER.info("Resource survey preparation: {} FULL chunks ready", preparedChunks);
|
||||
}
|
||||
}
|
||||
long preparedNanos = System.nanoTime();
|
||||
Map<BlockState, Long> observedStates = new IdentityHashMap<>();
|
||||
List<Map<String, Object>> chunks = new ArrayList<>();
|
||||
long totalCells = 0;
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
LevelChunk chunk = requireFullChunk(helper, level, x, z);
|
||||
Map<BlockState, Long> chunkStates = new IdentityHashMap<>();
|
||||
for (LevelChunkSection section : chunk.getSections()) {
|
||||
// This counts every cell in each palette, including all forms of air. Registry
|
||||
// lookups and property serialization happen once per distinct state later.
|
||||
section.getStates().count((state, count) -> chunkStates.merge(state, (long) count, Long::sum));
|
||||
}
|
||||
long chunkCells = sum(chunkStates);
|
||||
helper.assertTrue(chunkCells == 256L * level.getHeight(),
|
||||
"Every cell in the full dimension height must be counted at chunk " + x + "," + z);
|
||||
long nonAir = 0;
|
||||
for (var entry : chunkStates.entrySet()) {
|
||||
observedStates.merge(entry.getKey(), entry.getValue(), Long::sum);
|
||||
if (!entry.getKey().isAir()) nonAir += entry.getValue();
|
||||
}
|
||||
chunks.add(Map.of("x", x, "z", z, "status", "minecraft:full", "counted_cells", chunkCells,
|
||||
"non_air_blocks", nonAir, "distinct_block_states", chunkStates.size()));
|
||||
totalCells += chunkCells;
|
||||
}
|
||||
}
|
||||
Map<String, Long> blocks = new TreeMap<>();
|
||||
Map<String, Long> states = new TreeMap<>();
|
||||
long air = 0;
|
||||
for (var entry : observedStates.entrySet()) {
|
||||
String block = blockName(entry.getKey());
|
||||
blocks.merge(block, entry.getValue(), Long::sum);
|
||||
states.put(stateName(entry.getKey(), block), entry.getValue());
|
||||
if (entry.getKey().isAir()) air += entry.getValue();
|
||||
}
|
||||
long expectedCells = expectedChunks * 256L * level.getHeight();
|
||||
helper.assertTrue(chunks.size() == expectedChunks && totalCells == expectedCells,
|
||||
"Survey completeness must match its explicit requested chunk rectangle and height");
|
||||
helper.assertTrue(sum(blocks) == totalCells && sum(states) == totalCells,
|
||||
"Registry block and full-state totals must account for every inspected cell");
|
||||
var settingsHolder = ((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator()).generatorSettings();
|
||||
var settings = settingsHolder.value();
|
||||
String modVersion = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
|
||||
.getMetadata().getVersion().getFriendlyString();
|
||||
Map<String, Object> report = new LinkedHashMap<>();
|
||||
report.put("schema_version", 1);
|
||||
report.put("purpose", "development_post_generation_resource_observation");
|
||||
report.put("mode", mode);
|
||||
report.put("seed", level.getSeed());
|
||||
report.put("minecraft_version", SharedConstants.getCurrentVersion().name());
|
||||
report.put("generator", Map.of("preset", capacity == null || capacity.players() == 20 ? "sanctuary:sanctuary" : "sanctuary:sanctuary_" + capacity.players(), "noise_settings", settingsHolder.getRegisteredName(),
|
||||
"mod_version", modVersion, "version", "sanctuary@" + modVersion,
|
||||
"noise_min_y", settings.noiseSettings().minY(),
|
||||
"noise_max_y_exclusive", settings.noiseSettings().minY() + settings.noiseSettings().height()));
|
||||
report.put("dimension", "minecraft:overworld");
|
||||
report.put("initial_spawn", Map.of("x", initialSpawn.pos().getX(), "y", initialSpawn.pos().getY(),
|
||||
"z", initialSpawn.pos().getZ()));
|
||||
report.put("remote_test_fixture", Map.of("x", fixture.getX(), "y", fixture.getY(), "z", fixture.getZ(),
|
||||
"verified_outside_bounds_with_margin_blocks", 1024));
|
||||
report.put("bounds", Map.of("min_x_inclusive", minimumX * 16, "max_x_exclusive", (maximumX + 1) * 16,
|
||||
"min_z_inclusive", minimumZ * 16, "max_z_exclusive", (maximumZ + 1) * 16,
|
||||
"min_y_inclusive", level.getMinY(), "max_y_exclusive", level.getMinY() + level.getHeight(),
|
||||
"min_chunk_x_inclusive", minimumX, "max_chunk_x_inclusive", maximumX,
|
||||
"min_chunk_z_inclusive", minimumZ, "max_chunk_z_inclusive", maximumZ));
|
||||
report.put("declared_island_envelope", Map.of("terrain_radius_blocks", terrainRadius,
|
||||
"decoration_margin_blocks", DECORATION_MARGIN, "bounding_rectangle_radius_blocks", envelopeRadius));
|
||||
report.put("completeness", Map.of("requested_chunks", expectedChunks, "inspected_full_chunks", chunks.size(),
|
||||
"complete_within_requested_bounds", true, "covers_declared_island_envelope", mode.equals("island"),
|
||||
"covers_entire_world", false, "extrapolated", false, "height_coverage", "entire_dimension",
|
||||
"basis", "all palette cells in every inspected FULL chunk, not density samples"));
|
||||
report.put("preparation", Map.of("halo_chunks", PREPARATION_HALO_CHUNKS, "full_chunks_prepared", preparedChunks,
|
||||
"halo_included_in_counts", false, "world", "fresh_disposable_gametest_world",
|
||||
"personal_saves_accessed", false, "player_inventory_included", false, "remote_test_fixture_included", false));
|
||||
report.put("counts", Map.of("all_cells", totalCells, "air_cells", air, "non_air_blocks", totalCells - air,
|
||||
"by_block_registry_id", blocks, "by_complete_block_state", states));
|
||||
report.put("aggregates", aggregates(observedStates, blocks));
|
||||
report.put("aggregate_semantics", "Counts are blocks, not drops or economic value; groups overlap and must not be added together.");
|
||||
report.put("inspected_chunks", chunks);
|
||||
report.put("exclusions", List.of("entities", "item_drops", "container_contents", "player_inventories",
|
||||
"other_dimensions", "future_expansions", "blocks_outside_requested_bounds", "future_growth_and_loot"));
|
||||
report.put("observation", Map.of("started_at_utc", started.toString(), "completed_at_utc", Instant.now().toString(),
|
||||
"game_time_at_start", initialGameTime, "game_time_at_end", level.getGameTime(),
|
||||
"generation_preparation_millis", (preparedNanos - startedNanos) / 1_000_000L,
|
||||
"counting_and_serialization_preparation_millis", (System.nanoTime() - preparedNanos) / 1_000_000L,
|
||||
"scope", "snapshot after FULL generation; no resource quotas or terrain correction applied"));
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
Path output = directory.resolve("resource-survey-" + (capacity == null ? "legacy" : "players-" + capacity.players()) + "-seed-" + level.getSeed() + "-" + mode + ".json");
|
||||
Files.writeString(output, JSON.toJson(report) + "\n");
|
||||
SanctuaryMod.LOGGER.info("Resource survey {} complete: {} FULL chunks, {} non-air blocks; {}",
|
||||
mode, chunks.size(), totalCells - air, output.toAbsolutePath());
|
||||
}
|
||||
|
||||
private static LevelChunk requireFullChunk(GameTestHelper helper, ServerLevel level, int x, int z) {
|
||||
ChunkAccess chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Survey requires a FULL chunk at " + x + "," + z);
|
||||
return (LevelChunk) chunk;
|
||||
}
|
||||
|
||||
private static Map<String, Object> aggregates(Map<BlockState, Long> states, Map<String, Long> blocks) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("stone", explicitBlocks(blocks, "minecraft:stone"));
|
||||
result.put("base_stone_overworld", taggedStates(states, state -> state.is(BlockTags.BASE_STONE_OVERWORLD),
|
||||
"#minecraft:base_stone_overworld"));
|
||||
result.put("logs", taggedStates(states, state -> state.is(BlockTags.LOGS), "#minecraft:logs"));
|
||||
result.put("leaves", taggedStates(states, state -> state.is(BlockTags.LEAVES), "#minecraft:leaves"));
|
||||
for (String ore : List.of("coal", "iron", "copper", "gold", "redstone", "lapis", "diamond", "emerald")) {
|
||||
result.put(ore + "_ore", explicitBlocks(blocks, "minecraft:" + ore + "_ore", "minecraft:deepslate_" + ore + "_ore"));
|
||||
}
|
||||
result.put("water_blocks", explicitBlocks(blocks, "minecraft:water"));
|
||||
result.put("water_containing_blocks", taggedStates(states, state -> state.getFluidState().is(FluidTags.WATER),
|
||||
"block states containing #minecraft:water, including waterlogged blocks"));
|
||||
result.put("lava_blocks", explicitBlocks(blocks, "minecraft:lava"));
|
||||
result.put("sand", explicitBlocks(blocks, "minecraft:sand", "minecraft:red_sand"));
|
||||
result.put("gravel", explicitBlocks(blocks, "minecraft:gravel"));
|
||||
result.put("clay", explicitBlocks(blocks, "minecraft:clay"));
|
||||
result.put("sugar_cane", explicitBlocks(blocks, "minecraft:sugar_cane"));
|
||||
result.put("soil", explicitBlocks(blocks, "minecraft:grass_block", "minecraft:dirt", "minecraft:coarse_dirt",
|
||||
"minecraft:rooted_dirt", "minecraft:podzol", "minecraft:mud"));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Object> explicitBlocks(Map<String, Long> blocks, String... names) {
|
||||
Map<String, Long> members = new TreeMap<>();
|
||||
for (String name : names) members.put(name, blocks.getOrDefault(name, 0L));
|
||||
return Map.of("count", sum(members), "definition", "sum of the listed block registry IDs", "members", members);
|
||||
}
|
||||
|
||||
private static Map<String, Object> taggedStates(Map<BlockState, Long> states, Predicate<BlockState> predicate,
|
||||
String definition) {
|
||||
Map<String, Long> members = new TreeMap<>();
|
||||
states.forEach((state, count) -> {
|
||||
if (predicate.test(state)) members.merge(blockName(state), count, Long::sum);
|
||||
});
|
||||
return Map.of("count", sum(members), "definition", definition, "observed_members", members);
|
||||
}
|
||||
|
||||
private static String blockName(BlockState state) {
|
||||
return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
}
|
||||
|
||||
private static String stateName(BlockState state, String block) {
|
||||
Map<String, String> properties = new TreeMap<>();
|
||||
for (Property<?> property : state.getProperties()) properties.put(property.getName(), propertyValue(state, property));
|
||||
if (properties.isEmpty()) return block;
|
||||
StringJoiner suffix = new StringJoiner(",", "[", "]");
|
||||
properties.forEach((name, value) -> suffix.add(name + "=" + value));
|
||||
return block + suffix;
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> String propertyValue(BlockState state, Property<T> property) {
|
||||
return property.getName(state.getValue(property));
|
||||
}
|
||||
|
||||
private static long sum(Map<?, Long> counts) {
|
||||
return counts.values().stream().mapToLong(Long::longValue).sum();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
-351
@@ -1,351 +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.ruins.RuinsPiece17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer17;
|
||||
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.EnumSet;
|
||||
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.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.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.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.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in native proof confined to the new generation17 starter island in a disposable development world. */
|
||||
public final class Ruins17WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void forgottenNetworksPersistNatively(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The forgotten-networks proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 17, "Use a fresh generation17 test world; do not convert old saves");
|
||||
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 RuinsPlan17 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.ruinsPlan17();
|
||||
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 starts, references, room blocks, player-sized routes and unopened loot on the starter island only. "
|
||||
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
|
||||
+ "is saved and checked in a second JVM. Open hatches are restored after traversal; no free-fluid simulation is claimed.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) {
|
||||
prepare(); prepared = true;
|
||||
if (plan.isEmpty()) { finishDisabled(); return; }
|
||||
}
|
||||
if (nextChunk < chunks.size()) {
|
||||
var pos = chunks.get(nextChunk++);
|
||||
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Forgotten networks17: loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
verifyPlaced();
|
||||
report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
|
||||
"Gameplay witness must replace a solid outer wall away from every doorway");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing 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("Forgotten networks17 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() == 17
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation17 codec must preserve its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
|
||||
helper.assertTrue(plan.samples() <= RuinsPlan17.MAX_SAMPLES, "Forgotten-network planning exceeded its budget");
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
|
||||
"Reload requires the successful, flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate 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 used another world, seed or size");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
|
||||
"Saved switch or planned generation17 room identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
|
||||
if (!enabled) {
|
||||
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no forgotten network");
|
||||
return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
|
||||
var families = EnumSet.noneOf(RuinsPlan17.Family.class);
|
||||
plan.networks().forEach(n -> families.add(n.family()));
|
||||
report.put("families", families.stream().map(Enum::name).toList()); report.put("networks", plan.networks().size());
|
||||
if (level.getSeed() == 0) helper.assertTrue(families.equals(EnumSet.allOf(RuinsPlan17.Family.class)),
|
||||
"Seed0 reference must demonstrate all three experimental families; found " + families);
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
for (var network : plan.networks()) {
|
||||
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
|
||||
if (!origins.add(origin)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.id());
|
||||
starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof RuinsPiece17, "Forgotten network loaded a foreign or historical piece type");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(uniqueChunks.size() <= 225, "The bounded root-only proof unexpectedly covers more than225 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the 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(RuinsStructures17.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var sites = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, supports = 0, chests = 0, pieces = 0;
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
|
||||
var expectedRooms = plan.networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z())
|
||||
.flatMap(n -> n.rooms().stream()).toList();
|
||||
var actualRooms = start.getPieces().stream().map(RuinsPiece17.class::cast).map(RuinsPiece17::room).toList();
|
||||
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
|
||||
"Native start did not persist every planned room exactly once");
|
||||
String nativeHash = hash(saved.toString());
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) {
|
||||
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
|
||||
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
|
||||
}
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
|
||||
"Reload altered native room parameters, supports or reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var piece : start.getPieces()) {
|
||||
var roomPiece = (RuinsPiece17) piece; var room = roomPiece.room(); pieces++;
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
|
||||
});
|
||||
var roomTag = roomPiece.createTag(context);
|
||||
var roundtrip = new RuinsPiece17(context, roomTag);
|
||||
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
|
||||
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryRoom").orElseThrow(); payload.putInt("Schema", 99);
|
||||
boolean rejected = false;
|
||||
try { new RuinsPiece17(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "An unknown generation17 piece schema must be refused");
|
||||
var expected = rendered(room); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue();
|
||||
helper.assertTrue(RuinsPiece17.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
|
||||
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
|
||||
}
|
||||
for (var support : room.supports()) {
|
||||
supports++;
|
||||
var below = new BlockPos(support.x(), support.minY() - 1, support.z());
|
||||
helper.assertTrue(level.getBlockState(below).isSolid() && level.getFluidState(below).isEmpty(), "Room footing has lost its native rock anchor");
|
||||
}
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
|
||||
"Native chest loot must remain deferred with its exact table and seed");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
if (!reload && gold == null && room.kind() != RuinsPlan17.Kind.ACCESS && room.kind() != RuinsPlan17.Kind.CORRIDOR)
|
||||
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
|
||||
}
|
||||
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
|
||||
"pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
var traversal = new ArrayList<Map<String, Object>>();
|
||||
for (var network : plan.networks()) traversal.add(verifyAccess(helper, network));
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(RuinsStructures17.KEY), "Native locate cannot find a saved forgotten network");
|
||||
helper.assertTrue(plan.networks().stream().flatMap(n -> n.rooms().stream()).filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||
.anyMatch(r -> RuinsPiece17.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
|
||||
report.put("starts", sites); 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("native_routes", traversal); 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(RuinsPlan17.Room room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
RuinsRenderer17.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 = RuinsPiece17.locator(access); var reached = walk(helper, start, inside);
|
||||
int targets = 0;
|
||||
for (var room : network.rooms()) for (var target : RuinsRenderer17.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 Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked native surface approach: " + 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 traversal exceeded its room 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 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 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("ruins17-create-proof.json"); }
|
||||
private static String planHash(RuinsPlan17 plan) { return hash(plan.networks().toString()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "ruins17-" + (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); }
|
||||
}
|
||||
}
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
|
||||
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
|
||||
import net.minecraft.tags.FluidTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.IslandShape;
|
||||
import fr.koka.sanctuary.worldgen.MainIslandDensity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
|
||||
/** Assertions use absolute world positions, away from the framework's random fixture grid. */
|
||||
public final class SanctuaryWorldGameTests {
|
||||
public static LevelData.RespawnData initialSpawn;
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void optimizedDensityPreservesSavedTerrain(GameTestHelper helper) throws Exception {
|
||||
PopulationDensityEquivalenceDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void populationChoiceSurvivesSavingTheWorld(GameTestHelper helper) throws java.io.IOException {
|
||||
PopulationPersistenceDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) PopulationDiagnostics.start(helper);
|
||||
else if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
|
||||
else HydrologyDiagnostics.start(helper);
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (PopulationHydrologyRuntime.enabled(generator)) PopulationDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void generatedIslandAndInitialSpawn(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
helper.assertTrue(SanctuarySpawn.usesSanctuaryGenerator(level),
|
||||
"GameTest must load the production Sanctuary preset, not a flat world");
|
||||
|
||||
helper.assertTrue(initialSpawn != null, "Capture the world's initial spawn before the test grid moves it");
|
||||
BlockPos spawn = initialSpawn.pos();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
IslandCapacity capacity = IslandCapacity.forGenerator(generator);
|
||||
int terrainLimit = capacity == null ? IslandShape.TERRAIN_LIMIT : capacity.terrainLimit();
|
||||
helper.assertTrue(Math.hypot((double) spawn.getX(), (double) spawn.getZ()) < terrainLimit,
|
||||
"Shared spawn must lie on the finite natural island: " + spawn);
|
||||
helper.assertTrue(spawn.getY() > 1 && spawn.getY() < level.getMaxY() - 1,
|
||||
"Shared spawn must be inside the dimension height");
|
||||
var settings = generator.generatorSettings().value();
|
||||
float spawnFloorDensity = level.getChunkSource().randomState().sampleBlockValueUncached(
|
||||
settings.noiseRouter().finalDensity(), spawn.getX(), spawn.getY() - 1, spawn.getZ());
|
||||
helper.assertTrue(spawnFloorDensity > 0.0F,
|
||||
"The spawn floor must already exist in natural terrain density, without adding a platform");
|
||||
// These regression seeds have a naturally flat 3x3 arrival area. Production also supports
|
||||
// a single safe column when a more irregular seed offers no sampled 3x3 floor.
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
BlockPos feet = spawn.offset(dx, 0, dz);
|
||||
BlockPos floor = feet.below();
|
||||
helper.assertTrue(level.getBlockState(floor).isCollisionShapeFullBlock(level, floor),
|
||||
"Every cell of the 3x3 arrival area needs a full solid floor: " + floor);
|
||||
for (int height = 0; height < 2; height++) {
|
||||
BlockPos space = feet.above(height);
|
||||
helper.assertTrue(level.getBlockState(space).getCollisionShape(level, space).isEmpty()
|
||||
&& level.getFluidState(space).isEmpty(),
|
||||
"The arrival area needs two dry unobstructed blocks: " + space);
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void sculptOnlyRemovesTerrain(GameTestHelper helper) throws java.io.IOException {
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null)
|
||||
PopulationDiagnostics.verifyDensity(helper);
|
||||
else DensityDiagnostics.verifyAndExport(helper, initialSpawn);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void generationHeightAndLegacySettingsStaySeparate(GameTestHelper helper) {
|
||||
var level = helper.getLevel();
|
||||
var registries = level.registryAccess();
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
helper.assertTrue(level.getMinY() == 0 && level.getHeight() == 384,
|
||||
"The new Sanctuary dimension must have exactly 384 buildable blocks, Y0..383");
|
||||
float cloudHeight = level.environmentAttributes()
|
||||
.getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.CLOUD_HEIGHT);
|
||||
helper.assertTrue(Math.abs(cloudHeight - 352.33F) < 0.001F,
|
||||
"Sanctuary clouds must be raised to Y352.33");
|
||||
helper.assertTrue(settings.getOrThrow(SanctuarySpawn.RAISED_SETTINGS).value().noiseSettings().height() == 384,
|
||||
"Noise generation must use all 384 blocks, not just a taller dimension");
|
||||
helper.assertTrue(settings.getOrThrow(SanctuarySpawn.SETTINGS).value().noiseSettings().height() == 256,
|
||||
"The noise settings referenced by saved alpha.2 worlds must retain their old height");
|
||||
var legacy = registries.lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("final_density"))).value();
|
||||
helper.assertTrue(legacy instanceof MainIslandDensity old && old.verticalOffset() == 0 && old.height() == 256,
|
||||
"The legacy density codec must remain in its unshifted alpha.2 mode");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void completedLowerMarginIsVoid(GameTestHelper helper) {
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null) {
|
||||
PopulationDiagnostics.verifyLowerMargin(helper);
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
for (int[] coordinate : new int[][]{{0, 0}, {-8, 0}, {8, 0}, {0, -8}, {0, 8}}) {
|
||||
ChunkAccess chunk = helper.getLevel().getChunkSource().getChunk(
|
||||
coordinate[0], coordinate[1], ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk != null, "The lower-margin test needs a fully generated chunk");
|
||||
if (chunk.getSection(0).hasOnlyAir()) continue;
|
||||
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator)
|
||||
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
|
||||
"The legacy island must leave its lower margin entirely empty");
|
||||
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
|
||||
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
BlockPos p = new BlockPos(x, y, z);
|
||||
var state = chunk.getBlockState(p);
|
||||
if (CavernHydrologyRuntime.enabled(generator)) {
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER)
|
||||
&& CavernDiagnostics.isWithinSpringInspectionFootprint(CavernHydrologyRuntime.cavernPlan(helper.getLevel()), x, z);
|
||||
final int bx = x, bz = z;
|
||||
boolean lava = state.getFluidState().is(FluidTags.LAVA)
|
||||
&& CavernHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
|
||||
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
|
||||
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
|
||||
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
|
||||
continue;
|
||||
}
|
||||
if (RiftHydrologyRuntime.enabled(generator)) {
|
||||
boolean water = state.getFluidState().is(FluidTags.WATER)
|
||||
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
|
||||
final int bx = x, bz = z;
|
||||
boolean lava = state.getFluidState().is(FluidTags.LAVA)
|
||||
&& RiftHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
|
||||
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
|
||||
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
|
||||
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
|
||||
continue;
|
||||
}
|
||||
helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER)
|
||||
&& (WoodlandHydrologyRuntime.enabled(generator)
|
||||
? WoodlandDiagnostics.isWithinSpringInspectionFootprint(WoodlandHydrologyRuntime.woodlandPlan(helper.getLevel()), x, z)
|
||||
: LayeredHydrologyRuntime.enabled(generator)
|
||||
? LayeredHydrologyDiagnostics.isWithinSpringInspectionFootprint(LayeredHydrologyRuntime.layeredPlan(helper.getLevel()), x, z)
|
||||
: NaturalHydrologyDiagnostics.isWithinSpringInspectionFootprint(NaturalHydrologyRuntime.naturalPlan(helper.getLevel()), x, z)),
|
||||
"The lower margin has no artificial floor; water must lie in the finite spring inspection footprint, with provenance checked by the full fluid diagnostic: " + p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void generatedResourceSurvey(GameTestHelper helper) throws java.io.IOException {
|
||||
ResourceSurveyDiagnostics.verifyAndExport(helper, initialSpawn);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void completedExteriorChunksAreVoid(GameTestHelper helper) {
|
||||
if (IslandCapacity.forGenerator(helper.getLevel().getChunkSource().getGenerator()) != null) {
|
||||
PopulationDiagnostics.verifyExterior(helper);
|
||||
helper.succeed();
|
||||
return;
|
||||
}
|
||||
ServerLevel level = helper.getLevel();
|
||||
// First row is just beyond the declared terrain/decorations envelope. The last
|
||||
// samples catch the legacy bug where the infinite archipelago returned farther out.
|
||||
int[][] chunks = {
|
||||
{20, 0}, {-21, 0}, {0, 20}, {0, -21},
|
||||
{32, 0}, {-33, 0}, {0, 32}, {0, -33},
|
||||
{256, 0}, {-257, 0}, {0, 256}, {0, -257}
|
||||
};
|
||||
for (int[] coordinate : chunks) {
|
||||
ChunkAccess chunk = level.getChunkSource().getChunk(coordinate[0], coordinate[1], ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "The test must inspect a fully generated chunk");
|
||||
for (LevelChunkSection section : chunk.getSections()) {
|
||||
helper.assertTrue(section.hasOnlyAir(),
|
||||
"Finished exterior chunk contains blocks after world generation: " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void compiledWorldDensityUsesTheSeed(GameTestHelper helper) {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var registries = level.registryAccess();
|
||||
NoiseGeneratorSettings settings = ((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator()).generatorSettings().value();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
RandomState first = RandomState.create(noises, 0L, settings);
|
||||
RandomState replay = RandomState.create(noises, 0L, settings);
|
||||
RandomState otherSeed = RandomState.create(noises, 8675309L, settings);
|
||||
DensityFunction density = settings.noiseRouter().finalDensity();
|
||||
boolean observedSeedVariation = false;
|
||||
for (int x : new int[]{112, 160, 208}) {
|
||||
for (int z : new int[]{-96, -48, 0, 48, 96}) {
|
||||
for (int y : new int[]{96, 128, 160, 192, 224}) {
|
||||
float value = first.sampleBlockValueUncached(density, x, y, z);
|
||||
float repeated = replay.sampleBlockValueUncached(density, x, y, z);
|
||||
float changed = otherSeed.sampleBlockValueUncached(density, x, y, z);
|
||||
helper.assertTrue(Float.isFinite(value) && Float.isFinite(changed),
|
||||
"Compiled world density must remain finite");
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(repeated),
|
||||
"The actual datapack density must replay identically for the same seed");
|
||||
observedSeedVariation |= Float.floatToIntBits(value) != Float.floatToIntBits(changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
helper.assertTrue(observedSeedVariation, "Different world seeds must change the production density");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
-156
@@ -1,156 +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.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.*;
|
||||
import fr.koka.sanctuary.worldgen.heritage.*;
|
||||
import fr.koka.sanctuary.worldgen.transit.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
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;
|
||||
|
||||
/** Read-only admission audit, then native starts and reload in the disposable development world. */
|
||||
public final class Selective22WorldGameTests {
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void naturalSitesAndRefusalsPersist(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 22, "Use a fresh22 development world");
|
||||
var root = ExpansionIsland.origin(generator.initialDiameter(), level.getSeed(), 22);
|
||||
var city = generator.cityPlacement21(root);
|
||||
var heritage = generator.heritagePlan19();
|
||||
var transit = generator.transitPlan21();
|
||||
helper.assertTrue(city != null && city.reservation() == null && city.supports().isEmpty(), "No forced city envelope or rescue piers");
|
||||
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
|
||||
var terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||
int columns = 0;
|
||||
for (var district : city.surface().districts()) for (var parcel : district.parcels()) {
|
||||
for (int x = parcel.minX(); x <= parcel.maxX(); x++) for (int z = parcel.minZ(); z <= parcel.maxZ(); z++) {
|
||||
int floor = parcel.surfaceY(x - parcel.minX(), z - parcel.minZ());
|
||||
helper.assertTrue(Math.abs(floor - terrain.surfaceY(x, z)) <= 2, "City must fit natural relief"); columns++;
|
||||
}
|
||||
}
|
||||
// The three samplers have independent bounded caches, just like production planning.
|
||||
terrain = new HeritageTerrain21(root, binding.generator(), binding.random(), level, p -> false);
|
||||
for (var site : heritage.sites()) {
|
||||
helper.assertTrue(site.supports().isEmpty(), "No heritage rescue piers");
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++) {
|
||||
helper.assertTrue(Math.abs(site.floorY() - terrain.surfaceY(x, z)) <= 2, "Building cut/fill stays within two blocks");
|
||||
helper.assertTrue(terrain.solid(x, site.minY() - 1, z) && terrain.solid(x, site.minY() - 2, z), "Every foundation column rests on original rock"); columns++;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(!transit.isEmpty(), "The reference island retains its core traversal");
|
||||
var hall = generator.undergroundHall22();
|
||||
helper.assertTrue(!hall.isEmpty() || generator.undergroundPlan16(root).networks().stream().flatMap(n -> n.modules().stream())
|
||||
.anyMatch(m -> m.kind() == LostCityUnderground16.Kind.EXPANSION_HALL), "Core hall exists with or without a surface town");
|
||||
for (var module : transit.modules()) for (var support : module.supports())
|
||||
helper.assertTrue(support.maxY() - support.minY() + 1 <= 16, "Core traversal retains bounded engineered supports");
|
||||
if (!generator.experimentalStructures()) helper.assertTrue(heritage.isEmpty() && generator.ruinsPlan17().isEmpty(), "OFF removes optional catalogue, not core hall and traversal");
|
||||
var gson = new GsonBuilder().setPrettyPrinting().create();
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, level.registryAccess()), generator).getOrThrow();
|
||||
var geometry = gson.toJsonTree(Map.of("city", city.surface().districts(), "heritage_sites", heritage.sites(),
|
||||
"heritage_links", heritage.links(), "heritage_segments", heritage.segments(), "transit", transit.modules(), "independent_hall", hall.networks()));
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("generator_codec", encoded); report.put("geometry", geometry);
|
||||
report.put("experimental", generator.experimentalStructures()); report.put("city_districts", city.surface().districts().size());
|
||||
report.put("heritage_sites", heritage.sites().size()); report.put("heritage_links", heritage.links().size()); report.put("transit_modules", transit.modules().size());
|
||||
report.put("independent_hall_networks", hall.networks().size());
|
||||
report.put("core_entries", transit.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).map(m -> List.of(m.centerX(), m.surfaceY() + 1, m.minZ() - 2)).toList());
|
||||
report.put("hall_entries", hall.networks().stream().map(n -> { var at = LostCityUndergroundStructures22.locator(n); return List.of(at.getX(), at.getY(), at.getZ()); }).toList());
|
||||
report.put("natural_columns_audited", columns); report.put("city_diagnostics", city.diagnostics());
|
||||
report.put("heritage_diagnostics", heritage.diagnostics()); report.put("transit_diagnostics", transit.diagnostic());
|
||||
var starts = new ArrayList<StructureStart>();
|
||||
var origins = new LinkedHashMap<ResourceKey<Structure>, Set<ChunkPos>>();
|
||||
origins.put(LostCityStructures.KEY, new HashSet<>()); origins.put(HeritageStructures19.KEY, new HashSet<>()); origins.put(TransitStructures21.KEY, new HashSet<>()); origins.put(LostCityUndergroundStructures22.KEY, new HashSet<>());
|
||||
city.surface().districts().forEach(d -> origins.get(LostCityStructures.KEY).add(new ChunkPos(d.startChunkX(), d.startChunkZ())));
|
||||
heritage.sites().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
heritage.segments().forEach(s -> origins.get(HeritageStructures19.KEY).add(new ChunkPos(s.startChunkX(), s.startChunkZ())));
|
||||
transit.modules().forEach(m -> origins.get(TransitStructures21.KEY).add(new ChunkPos(m.startChunkX(), m.startChunkZ())));
|
||||
hall.networks().forEach(n -> origins.get(LostCityUndergroundStructures22.KEY).add(new ChunkPos(n.startChunkX(), n.startChunkZ())));
|
||||
var chunks = new TreeSet<ChunkPos>(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z));
|
||||
var nativeTags = new TreeMap<String, String>();
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
for (var entry : origins.entrySet()) for (var origin : entry.getValue()) {
|
||||
var type = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(entry.getKey()).value();
|
||||
var start = level.getChunk(origin.x(), origin.z(), ChunkStatus.STRUCTURE_STARTS, true).getStartForStructure(type);
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted geometry must have a native start");
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(!(piece instanceof LostCityFoundationPiece16), "No artificial city foundation emitted");
|
||||
if (entry.getKey().equals(HeritageStructures19.KEY)) helper.assertTrue(piece instanceof HeritagePiece21, "Use published21 architecture");
|
||||
if (entry.getKey().equals(LostCityUndergroundStructures22.KEY)) helper.assertTrue(piece instanceof LostCityUndergroundPiece20, "Independent hall keeps published20 room geometry");
|
||||
if (entry.getKey().equals(TransitStructures21.KEY)) helper.assertTrue(piece instanceof TransitPiece21 || piece instanceof TransitPiece22, "Published21 tunnel geometry with sealed22 corners");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(chunks::add);
|
||||
}
|
||||
var tag = start.createTag(context, origin);
|
||||
var loaded = StructureStart.loadStaticStart(context, tag, level.getSeed());
|
||||
helper.assertTrue(loaded != null && tag.equals(loaded.createTag(context, origin)), "Native start round trip");
|
||||
nativeTags.put(entry.getKey().identifier() + "/" + origin.x() + "/" + origin.z(), tag.toString()); starts.add(start);
|
||||
}
|
||||
if (chunks.isEmpty()) chunks.add(new ChunkPos(0, 0));
|
||||
report.put("native_starts", nativeTags); report.put("full_chunks", chunks.size());
|
||||
Path world = level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
Path proof = world.resolve("selective22-create.json");
|
||||
boolean reload = Boolean.getBoolean("sanctuary.test.expansionReload");
|
||||
JsonObject previous = reload ? JsonParser.parseString(Files.readString(proof)).getAsJsonObject() : null;
|
||||
if (reload) {
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload follows a successful separate JVM");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("geometry").equals(geometry), "Generation, options and accepted/refused placements survive reload");
|
||||
helper.assertTrue(previous.get("native_starts").equals(gson.toJsonTree(nativeTags)), "Native saved starts survive reload");
|
||||
}
|
||||
var iterator = chunks.iterator();
|
||||
Runnable finish = () -> {
|
||||
try {
|
||||
// Refresh already-generated transit chunks: the earlier short FULL tickets can expire
|
||||
// while the catalogue loads. The walk checks real native collision shapes, including turns.
|
||||
var transitChunks = new HashSet<ChunkPos>();
|
||||
transit.modules().forEach(m -> TransitPiece21.bounds(m).intersectingChunks().forEach(transitChunks::add));
|
||||
for (var pos : transitChunks) level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
report.put("transit_native_walk", Transit21WorldGameTests.verifyAccess(helper, transit));
|
||||
report.put("hall_native_access", Underground22NativeChecks.verify(helper, hall.isEmpty() ? generator.undergroundPlan16(root) : hall));
|
||||
if (!heritage.sites().isEmpty()) {
|
||||
var site = heritage.sites().getFirst();
|
||||
var witness = new BlockPos(site.centerX(), site.floorY() - 1, site.centerZ());
|
||||
if (reload) helper.assertTrue(level.getBlockState(witness).is(Blocks.GOLD_BLOCK), "Personal edit in disposable test survives reload");
|
||||
else {
|
||||
helper.assertTrue(level.getBlockState(witness).isSolid(), "Accepted building foundation generated in real FULL chunks");
|
||||
level.setBlock(witness, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
}
|
||||
report.put("saved_edit", List.of(witness.getX(), witness.getY(), witness.getZ()));
|
||||
}
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native world before shutdown");
|
||||
report.put("passed", true);
|
||||
var directory = Path.of("diagnostics", "selective22-" + level.getSeed() + "-" + generator.initialDiameter()); Files.createDirectories(directory);
|
||||
var text = gson.toJson(report) + "\n"; Files.writeString(directory.resolve(reload ? "reload.json" : "create.json"), text);
|
||||
if (!reload) Files.writeString(proof, text);
|
||||
helper.succeed();
|
||||
} catch (Exception failure) { throw new IllegalStateException(failure); }
|
||||
};
|
||||
Runnable[] load = new Runnable[1];
|
||||
load[0] = () -> {
|
||||
if (!iterator.hasNext()) { finish.run(); return; }
|
||||
var pos = iterator.next(); level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> load[0].run());
|
||||
};
|
||||
load[0].run();
|
||||
}
|
||||
}
|
||||
-453
@@ -1,453 +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.transit.TransitPiece21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitPlan21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitRenderer21;
|
||||
import fr.koka.sanctuary.worldgen.transit.TransitStructures21;
|
||||
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.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.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.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
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.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.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in native proof confined to the new generation21 starter island in a disposable development world. */
|
||||
public final class Transit21WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void industrialTransitPersistsNatively(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The industrial-transits 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 old saves");
|
||||
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 TransitPlan21 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
final Map<BlockPos, BlockState> allGeometry = new HashMap<>();
|
||||
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.transitPlan21();
|
||||
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("planning_diagnostic", plan.diagnostic());
|
||||
report.put("scope", "Native starts, references, geometry, rock bearings, unopened loot and a player-sized stair walk between two surface gates. "
|
||||
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
|
||||
+ "is saved and checked in a second JVM. Traversal remains inside the modules and certified portico approaches; underground one-block rises need real correctly oriented stairs. Only the certified natural portico approaches permit an ordinary one-block jump. No flight, ladder or external terrain shortcut is allowed. No fluid simulation is claimed.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) {
|
||||
prepare(); prepared = true;
|
||||
if (plan.isEmpty()) { finishDisabled(); return; }
|
||||
}
|
||||
if (nextChunk < chunks.size()) {
|
||||
var pos = chunks.get(nextChunk++);
|
||||
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Industrial transit21: loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
verifyPlaced();
|
||||
report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
|
||||
"Gameplay witness must replace a solid outer wall away from every doorway");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing 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("Industrial transit21 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(), "The generation21 codec must preserve its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
|
||||
helper.assertTrue(plan.samples() <= TransitPlan21.MAX_SAMPLES, "Transit planning exceeded its budget");
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
|
||||
"Reload requires the successful, flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate 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 used another world, seed or size");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
|
||||
"Saved switch or planned generation21 room identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
|
||||
if (!enabled) {
|
||||
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no industrial transit");
|
||||
return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
|
||||
helper.assertTrue(plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).count() == 2,
|
||||
"A through-route needs two independent saved surface stairs");
|
||||
int spanX = plan.modules().stream().mapToInt(TransitPlan21.Module::maxX).max().orElseThrow()
|
||||
- plan.modules().stream().mapToInt(TransitPlan21.Module::minX).min().orElseThrow() + 1;
|
||||
int spanZ = plan.modules().stream().mapToInt(TransitPlan21.Module::maxZ).max().orElseThrow()
|
||||
- plan.modules().stream().mapToInt(TransitPlan21.Module::minZ).min().orElseThrow() + 1;
|
||||
report.put("span_x", spanX); report.put("span_z", spanZ);
|
||||
helper.assertTrue(Math.max(spanX, spanZ) >= generator.initialDiameter() * .4,
|
||||
"The native transit reference must cross at least40% of the island's nominal diameter");
|
||||
report.put("modules", plan.modules().size());
|
||||
report.put("halls", plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.HALL).count());
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
for (var network : plan.modules()) {
|
||||
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
|
||||
if (!origins.add(origin)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.kind() + " at " + network.centerX() + "," + network.centerZ());
|
||||
starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof TransitPiece21, "Industrial transit loaded a foreign or historical piece type");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
helper.assertTrue(Math.abs(at.x() - network.startChunkX()) <= 7 && Math.abs(at.z() - network.startChunkZ()) <= 7,
|
||||
"Transit native start exceeds its local reference range");
|
||||
uniqueChunks.add(at);
|
||||
});
|
||||
}
|
||||
}
|
||||
helper.assertTrue(uniqueChunks.size() <= 256, "The bounded root-only proof unexpectedly covers more than256 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
|
||||
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the 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(TransitStructures21.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var sites = new ArrayList<Map<String, Object>>();
|
||||
var modules = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, supports = 0, chests = 0, pieces = 0;
|
||||
var bearingMasks = new HashMap<TransitPlan21.Module, Integer>();
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
|
||||
var expectedRooms = plan.modulesForChunk(origin.x(), origin.z());
|
||||
var actualRooms = start.getPieces().stream().map(TransitPiece21.class::cast).map(TransitPiece21::module).toList();
|
||||
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
|
||||
"Native start did not persist every planned room exactly once");
|
||||
String nativeHash = hash(saved.toString());
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) {
|
||||
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
|
||||
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
|
||||
}
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
|
||||
"Reload altered native room parameters, supports or reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var piece : start.getPieces()) {
|
||||
var roomPiece = (TransitPiece21) piece; var room = roomPiece.module(); pieces++;
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
|
||||
});
|
||||
var roomTag = roomPiece.createTag(context);
|
||||
var roundtrip = new TransitPiece21(context, roomTag);
|
||||
helper.assertTrue(roundtrip.module().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
|
||||
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryModule").orElseThrow(); payload.putInt("Schema", 99);
|
||||
boolean rejected = false;
|
||||
try { new TransitPiece21(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "An unknown generation21 piece schema must be refused");
|
||||
var expected = rendered(room); blocks += expected.blocks().size();
|
||||
expected.blocks().forEach((at, state) -> helper.assertTrue(allGeometry.putIfAbsent(at, state) == null,
|
||||
"Two saved transit modules paint the same position: " + at));
|
||||
modules.add(Map.of("kind", room.kind().name(), "min", List.of(room.minX(), room.minY(), room.minZ()),
|
||||
"max", List.of(room.maxX(), room.maxY(), room.maxZ()), "floor_y", room.floorY(),
|
||||
"surface_y", room.surfaceY(), "openings", room.openings(), "supports", room.supports().size(),
|
||||
"start_chunk", List.of(room.startChunkX(), room.startChunkZ())));
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue();
|
||||
helper.assertTrue(TransitPiece21.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
|
||||
helper.assertTrue(plan.protects(at.getX(), at.getY(), at.getZ()) && generator.protectsCityDecoration(at),
|
||||
"Transit construction/cleared air is missing its published decoration protection at " + at);
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
|
||||
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
|
||||
}
|
||||
supports += verifyBearings(helper, generator, room, bearingMasks);
|
||||
for (var entry : expected.blocks().entrySet()) if (entry.getValue().is(Blocks.LANTERN)) {
|
||||
var at = entry.getKey(); var state = level.getBlockState(at);
|
||||
helper.assertTrue(state.canSurvive(level, at) && level.getBlockState(at.above()).isFaceSturdy(level, at.above(), Direction.DOWN),
|
||||
"Transit lamp lost its complete ceiling bearing at " + at);
|
||||
}
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
|
||||
"Native chest loot must remain deferred with its exact table and seed");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
if (!reload && gold == null && room.kind() != TransitPlan21.Kind.ACCESS && room.kind() != TransitPlan21.Kind.JUNCTION)
|
||||
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
|
||||
}
|
||||
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
|
||||
"pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
report.put("module_coordinates", modules);
|
||||
report.put("starts", sites);
|
||||
int weakSpan = 0, longestSpan = 0;
|
||||
for (var room : plan.modules()) {
|
||||
weakSpan = bearingMasks.get(room) == 15 ? 0 : weakSpan + 1;
|
||||
longestSpan = Math.max(longestSpan, weakSpan);
|
||||
helper.assertTrue(weakSpan <= 2, "Native deck spans more than 34 blocks without distributed rock bearings");
|
||||
}
|
||||
report.put("longest_span_without_full_bearings", longestSpan * 17);
|
||||
var ores = new java.util.TreeMap<String, Integer>();
|
||||
allGeometry.values().forEach(state -> {
|
||||
var id = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if (id.endsWith("_ore")) ores.merge(id, 1, Integer::sum);
|
||||
});
|
||||
report.put("finite_exposed_ores", ores);
|
||||
var traversal = verifyAccess(helper, plan);
|
||||
report.put("native_connections", NativeConnections20.verify(helper, allGeometry,
|
||||
reload && gold != null ? Set.of(gold) : Set.of()));
|
||||
report.put("module_coordinates", modules);
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(TransitStructures21.KEY), "Native locate cannot find a saved industrial transit");
|
||||
helper.assertTrue(plan.modules().stream().filter(r -> r.kind() == TransitPlan21.Kind.ACCESS)
|
||||
.anyMatch(r -> TransitPiece21.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
|
||||
report.put("starts", sites); 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("native_routes", traversal); 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(TransitPlan21.Module room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
TransitRenderer21.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 int verifyBearings(GameTestHelper helper, SanctuaryChunkGenerator generator, TransitPlan21.Module room, Map<TransitPlan21.Module, Integer> masks) {
|
||||
var level = helper.getLevel(); int checked = 0, quarters = 0;
|
||||
for (int z = room.minZ();; z = Math.min(z + 4, room.maxZ())) {
|
||||
for (int x = room.minX();; x = Math.min(x + 4, room.maxX())) {
|
||||
int anchor = room.minY() - 1;
|
||||
for (var support : room.supports()) if (support.x() == x && support.z() == z) anchor = support.minY() - 1;
|
||||
var bearing = new BlockPos(x, anchor, z);
|
||||
boolean supported = level.getBlockState(bearing).isSolid() && level.getFluidState(bearing).isEmpty()
|
||||
&& level.getBlockState(bearing.below()).isSolid() && level.getFluidState(bearing.below()).isEmpty();
|
||||
if (supported) {
|
||||
quarters |= 1 << ((x < room.centerX() ? 0 : 1) + (z < room.centerZ() ? 0 : 2));
|
||||
for (int dy = 0; dy >= -1; dy--) {
|
||||
var at = new BlockPos(x, anchor + dy, z);
|
||||
helper.assertTrue(level.getBlockState(at).isSolid() && level.getFluidState(at).isEmpty(),
|
||||
"Transit bearing grid lost one of its two untouched rock layers at " + at);
|
||||
helper.assertTrue(generator.protectsCityDecoration(at), "A certified transit rock dependency is unprotected at " + at);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
if (x == room.maxX()) break;
|
||||
}
|
||||
if (z == room.maxZ()) break;
|
||||
}
|
||||
helper.assertTrue(room.kind() == TransitPlan21.Kind.GALLERY || room.kind() == TransitPlan21.Kind.JUNCTION || quarters == 15, "The continuous deck lost its distributed rock bearings");
|
||||
masks.put(room, quarters);
|
||||
return checked;
|
||||
}
|
||||
static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
|
||||
var level = helper.getLevel();
|
||||
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
|
||||
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
|
||||
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||
var start = TransitPiece21.locator(accesses.getFirst());
|
||||
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
|
||||
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
|
||||
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|
||||
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
|
||||
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
|
||||
var arrivals = new ArrayList<Map<String, Object>>();
|
||||
for (var module : plan.modules()) for (var target : TransitRenderer21.targets(module)) {
|
||||
var point = at(module, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.containsKey(Foot.at(point)), "Native transit cannot walk from its first portico to "
|
||||
+ module.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var access : accesses) {
|
||||
var gate = TransitPiece21.locator(access);
|
||||
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
|
||||
int approaches = 0;
|
||||
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
|
||||
var high = new BlockPos(x, access.surfaceY() + 1, z);
|
||||
var low = high.below(); var position = standable(level, high) ? high : low;
|
||||
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
|
||||
"The certified natural approach no longer joins its portico at " + position);
|
||||
helper.assertTrue(level.getBlockState(position.below()).isSolid()
|
||||
&& level.getFluidState(position.below()).isEmpty(), "A surface arrival is suspended above air or water");
|
||||
approaches++;
|
||||
}
|
||||
arrivals.add(Map.of("gate", coords(gate), "roadway", coords(at(access, 8, 2, 8)),
|
||||
"rise", access.surfaceY() - access.floorY(), "steps_from_first_gate", reached.get(Foot.at(gate)),
|
||||
"natural_approach_cells", approaches));
|
||||
}
|
||||
return Map.of("surface_accesses", arrivals, "walkable_targets", targets, "standing_cells", reached.size(),
|
||||
"surface_to_surface_steps", reached.get(Foot.at(TransitPiece21.locator(accesses.getLast()))),
|
||||
"traversal_policy", "0.6 by1.8 native collision volume, half-block sidewalk steps; underground rises require a real forward-facing stair; natural portico approaches alone permit a normal one-block jump; no vertical moves");
|
||||
}
|
||||
/** Half-block standing elevations retain real sidewalk slabs without pretending an integer cell can hover over them. */
|
||||
private record Foot(int x, int halfY, int z) {
|
||||
static Foot at(BlockPos pos) { return new Foot(pos.getX(), pos.getY() * 2, pos.getZ()); }
|
||||
double y() { return halfY / 2.0; }
|
||||
BlockPos block() { return new BlockPos(x, Math.floorDiv(halfY, 2), z); }
|
||||
BlockPos bearing() { return new BlockPos(x, Math.floorDiv(halfY - 1, 2), z); }
|
||||
Foot rise(int halves) { return new Foot(x, halfY + halves, z); }
|
||||
Foot move(Direction direction, int halves) { return new Foot(x + direction.getStepX(), halfY + halves, z + direction.getStepZ()); }
|
||||
}
|
||||
private static Map<Foot, Integer> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside, Predicate<BlockPos> naturalApproach) {
|
||||
var level = helper.getLevel(); var first = Foot.at(start);
|
||||
helper.assertTrue(inside.test(start) && standable(level, first), "Blocked native surface portico: " + start);
|
||||
var seen = new HashMap<Foot, Integer>(); var queue = new ArrayDeque<Foot>(); seen.put(first, 0); queue.add(first);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -2; dy <= 2; dy++) {
|
||||
var next = pos.move(direction, dy);
|
||||
if (!inside.test(next.block()) || seen.containsKey(next)) continue;
|
||||
boolean ordinaryThreshold = naturalApproach.test(pos.block()) || naturalApproach.test(next.block());
|
||||
// Native step height is enough for a half slab; a full block needs the two treads of a stair.
|
||||
if (dy > 0 && (!clearBody(level, pos.rise(dy))
|
||||
|| dy == 2 && !ordinaryThreshold && !ascendingStair(level, next.bearing(), direction))) continue;
|
||||
if (dy < 0 && (!clearBody(level, next.rise(-dy))
|
||||
|| dy == -2 && !ordinaryThreshold && !ascendingStair(level, pos.bearing(), direction.getOpposite()))) continue;
|
||||
if (standable(level, next)) { seen.put(next, seen.get(pos) + 1); queue.add(next); }
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 120_000, "Native transit traversal exceeded its bounded room volumes");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean ascendingStair(ServerLevel level, BlockPos pos, Direction direction) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getBlock() instanceof StairBlock
|
||||
&& state.getValue(BlockStateProperties.HORIZONTAL_FACING) == direction
|
||||
&& state.getValue(BlockStateProperties.HALF) == net.minecraft.world.level.block.state.properties.Half.BOTTOM;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) { return standable(level, Foot.at(pos)); }
|
||||
private static boolean standable(ServerLevel level, Foot pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
var below = pos.bearing(); double top = pos.y() - below.getY();
|
||||
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> Math.abs(b.maxY - top) < .0001 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, Foot pos) {
|
||||
var body = new AABB(pos.x() + .2, pos.y(), pos.z() + .2, pos.x() + .8, pos.y() + 1.8, pos.z() + .8);
|
||||
for (int y = (int) Math.floor(pos.y()); y <= (int) Math.floor(pos.y() + 1.8 - .0001); y++) {
|
||||
var at = new BlockPos(pos.x(), y, pos.z());
|
||||
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 BlockPos at(TransitPlan21.Module room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
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("transit21-create-proof.json"); }
|
||||
private static String planHash(TransitPlan21 plan) { return hash(plan.modules().toString()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "transit21-" + (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); }
|
||||
}
|
||||
}
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Module;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** Native circulation proof on the already generated piece footprint. */
|
||||
public final class Underground22NativeChecks {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private Underground22NativeChecks() {}
|
||||
public static Map<String, Integer> verify(GameTestHelper helper, LostCityUnderground16.Plan plan) {
|
||||
var level = helper.getLevel(); var chunks = new HashSet<Long>();
|
||||
int halls = 0, ladders = 0, apertures = 0, walked = 0;
|
||||
helper.assertTrue(!plan.isEmpty(), "Independent underground hall plan must be present");
|
||||
for (var network : plan.networks()) {
|
||||
for (var m : network.modules()) {
|
||||
for (int cx = m.minX() >> 4; cx <= m.maxX() >> 4; cx++) for (int cz = m.minZ() >> 4; cz <= m.maxZ() >> 4; cz++) {
|
||||
long key = ((long) cx << 32) ^ (cz & 0xffffffffL);
|
||||
// Short-lived read tickets can expire during the caller’s asynchronous footprint load.
|
||||
// Refresh only these already generated chunks before inspecting their blocks.
|
||||
if (chunks.add(key)) helper.assertTrue(level.getChunk(cx, cz, ChunkStatus.FULL, true) instanceof LevelChunk,
|
||||
"Underground22 piece has no loaded FULL chunk at " + cx + "," + cz);
|
||||
}
|
||||
for (int d = 0; d < 4; d++) if ((m.openings() & 1 << d) != 0) {
|
||||
for (int side = -1; side <= 1; side++) for (int dy = 1; dy <= 3; dy++) {
|
||||
var at = mouth(m, d, side, dy);
|
||||
helper.assertTrue(passable(level.getBlockState(at)), "Native utility doorway blocked at " + at + " in " + m.kind());
|
||||
}
|
||||
apertures++;
|
||||
}
|
||||
}
|
||||
var access = network.modules().stream().filter(m -> m.kind() == Kind.ACCESS).findFirst().orElseThrow();
|
||||
BlockPos hatch = null;
|
||||
for (int x = access.minX(); x <= access.maxX(); x++) for (int z = access.minZ(); z <= access.maxZ(); z++) {
|
||||
var at = new BlockPos(x, access.maxY() - 1, z);
|
||||
if (level.getBlockState(at).is(Blocks.DARK_OAK_TRAPDOOR)) {
|
||||
helper.assertTrue(hatch == null, "The access has more than one surface hatch"); hatch = at;
|
||||
}
|
||||
}
|
||||
helper.assertTrue(hatch != null, "Independent access has no real wooden surface hatch");
|
||||
final BlockPos entrance = hatch;
|
||||
var closed = level.getBlockState(entrance);
|
||||
for (int y = access.floorY() + 1; y < entrance.getY(); y++) {
|
||||
var at = new BlockPos(entrance.getX(), y, entrance.getZ()); var ladder = level.getBlockState(at);
|
||||
helper.assertTrue(ladder.is(Blocks.LADDER), "Access ladder is interrupted at " + at);
|
||||
var wall = at.relative(ladder.getValue(BlockStateProperties.HORIZONTAL_FACING).getOpposite());
|
||||
helper.assertTrue(level.getBlockState(wall).isCollisionShapeFullBlock(level, wall), "Ladder has no real backing wall at " + at);
|
||||
ladders++;
|
||||
}
|
||||
level.setBlock(entrance, closed.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
try {
|
||||
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(entrance::relative)
|
||||
.filter(inside).filter(pos -> standable(level, pos)).findFirst().orElseThrow();
|
||||
var reached = walk(helper, approach, inside); walked += reached.size();
|
||||
helper.assertTrue(reached.contains(entrance), "Public deck cannot reach its hatch");
|
||||
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()),
|
||||
"Surface access cannot reach emitted utility module " + m.kind() + " at " + m.centerX() + "," + m.centerZ());
|
||||
if (m.kind() != Kind.EXPANSION_HALL) continue;
|
||||
halls++;
|
||||
var plinth = new BlockPos(m.centerX(), m.minY() + 2, m.centerZ());
|
||||
helper.assertTrue(level.getBlockState(plinth).is(Blocks.SMOOTH_STONE) && reached.contains(plinth.above()),
|
||||
"Reserved expansion plinth must be emitted and reachable");
|
||||
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)),
|
||||
"Expansion hall has a blocked quarter");
|
||||
}
|
||||
} finally { level.setBlock(entrance, closed, 2); }
|
||||
}
|
||||
helper.assertTrue(halls > 0, "Native networks have no usable expansion hall");
|
||||
var report = new LinkedHashMap<String, Integer>(); report.put("full_chunks", chunks.size()); report.put("usable_halls", halls);
|
||||
report.put("ladder_blocks", ladders); report.put("clear_openings", apertures); report.put("reached_positions", walked);
|
||||
return Map.copyOf(report);
|
||||
}
|
||||
private static BlockPos mouth(Module m, int direction, int across, int dy) {
|
||||
return new BlockPos(direction == 1 ? m.maxX() : direction == 3 ? m.minX() : m.centerX() + across,
|
||||
m.floorY() + dy, direction == 0 ? m.minZ() : direction == 2 ? m.maxZ() : m.centerZ() + across);
|
||||
}
|
||||
/** Same finite walking/ladder rule used by the native city16/20/21 access proofs. */
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); var reached = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>();
|
||||
helper.assertTrue(inside.test(start) && standable(level, start), "Blocked utility approach"); reached.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.remove();
|
||||
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 && !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) && (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|
||||
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && reached.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(reached.size() < 100_000, "Utility traversal exceeded its bounded footprint");
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
var below = level.getBlockState(pos.below()); var at = level.getBlockState(pos);
|
||||
return passable(at) && passable(level.getBlockState(pos.above()))
|
||||
&& (below.isSolid() || below.getBlock() instanceof StairBlock || at.is(Blocks.LADDER) || below.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_TRAPDOOR) && state.getValue(BlockStateProperties.OPEN);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user