Compare commits

..
Author SHA1 Message Date
koka 9c096b2563 Deliver beta.233 with verified community forms and Vulkan layouts
Build Sanctuary / build (push) Canceled after 0s
2026-10-06 03:59:50 +02:00
koka bc27606b3e Simplify notices and place Gazette photos before the title 2026-10-06 03:57:17 +02:00
koka 8531bb3476 Group pause navigation consistently with and without the map 2026-10-06 03:57:17 +02:00
koka 8e9b8fbd62 Deliver beta.232 with verified solo gameplay and Vulkan client checks 2026-10-06 03:25:17 +02:00
koka 308c0dc2ff Center the locked Atlas pause menu between Gazette and Notice Board 2026-10-06 03:24:47 +02:00
koka 50aaa5c135 Exclude generated diamonds from initial island chest loot 2026-10-06 03:24:47 +02:00
koka cf5699cfe1 Continue held vein mining on the next visible group after server completion 2026-10-06 03:24:46 +02:00
koka d9979100a9 Align placement outlines with building reach from three to seven blocks 2026-10-06 03:24:46 +02:00
koka 5f3d110773 Allow the solo owner to choose a persistent world timezone 2026-10-06 03:24:33 +02:00
koka a2201cc422 beta.231: enrich pale understory and prepare nine-island lab
Build Sanctuary / build (push) Canceled after 0s
2026-10-05 23:08:05 +02:00
koka 42e42fea2a beta.230: populate lower pale terraces and mix yellow savanna rock 2026-10-05 22:45:45 +02:00
koka 55df522b1e beta.229: rebuild northeast wetland above sheltered pale groves 2026-10-05 22:24:29 +02:00
koka b77d71bd0d Lower pale wetlands and facet diagonal reefs for beta.228 2026-10-05 21:55:12 +02:00
koka 70a34df2d5 Refine diagonal shores and cave habitats for beta.227 2026-10-05 21:34:48 +02:00
koka 5fb1407705 beta.226: shape four distinct diagonal landscapes 2026-10-05 20:34:13 +02:00
koka e2b9ee7aaf feat(worldgen): add 1024 climate diagonals and island sugar cane in beta.225 2026-10-05 19:44:33 +02:00
koka 1388d8a443 docs: confirm all four islands in standard Sanctuary beta.224 2026-10-05 17:34:55 +02:00
koka 4554ea1882 fix(worldgen): ground giant trees and stripe the East crater in beta.224 2026-10-05 17:29:49 +02:00
koka 9063e16e7c feat(worldgen): add natural floating jungle volcano in beta.223 2026-10-05 17:11:17 +02:00
koka 2d656d9525 feat(building): add yellow rock stairs and slabs in beta.222 2026-10-05 16:50:38 +02:00
koka a3dd17c37a feat(worldgen): refine western cliffs and add yellow stone in beta.221 2026-10-05 16:34:55 +02:00
koka 58aa4c2ae1 feat(worldgen): sculpt a living western coast in beta.220 2026-10-05 16:09:03 +02:00
koka 988a6c8f51 feat(worldgen): lower western beaches and move palms in beta.219 2026-10-05 15:17:07 +02:00
koka 3adb08eaa6 feat(worldgen): reshape western ocean into an oasis in beta.218 2026-10-05 14:52:25 +02:00
koka f8456df256 feat(worldgen): add suspended western sea in beta.217 2026-10-05 14:15:02 +02:00
koka 0eb4517961 beta.216: forests, surface trails and trapped pyramid in the South 2026-10-05 08:37:08 +02:00
koka 03aab49996 Add raised regional relay monuments and enlarge the southern pyramid for beta.215 2026-10-05 07:50:14 +02:00
koka b82ab378f0 Add abandoned dark oak western village to South beta.214 2026-10-05 07:21:47 +02:00
koka da838b9e8e beta.213: bury the southern temple beneath a local terracotta pyramid 2026-10-05 06:33:30 +02:00
koka 3941bd4711 beta.212: expose radial Badlands and add a trapped desert complex 2026-10-05 06:11:04 +02:00
koka 7375046488 Rebuild southern badlands with shattered native noise for beta.211 2026-10-05 05:29:42 +02:00
koka 2ee53e1065 Refine southern canyon palettes and dry ecology for beta.210 2026-10-05 05:03:50 +02:00
koka efb1097b65 feat(worldgen): prototype multicoloured South canyons in beta.209 2026-10-05 04:26:51 +02:00
koka d021341d3e Sharpen northern ridges and add supported glacial benches for beta.208 2026-10-05 03:24:32 +02:00
koka bb10c24ada Add patchy northern snow, local crowns and spruce mansion for beta.207 2026-10-05 02:29:06 +02:00
koka 6f780547bb Shape thinner northern shelves and wet amethyst caves for beta.206 2026-10-05 01:32:41 +02:00
koka bf5df3885b Use unwarped native noise and continuous northern ecology for beta.205 2026-10-05 00:37:26 +02:00
koka 2463db72f7 Integrate Demeure and Sanctuary terrain with northern Peaks for beta.204 2026-10-05 00:07:08 +02:00
765 changed files with 36446 additions and 539 deletions
+294
View File
@@ -1,5 +1,299 @@
# Sanctuary
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.233 — Navigation centrée et rédaction simplifiée
[Ajustements du menu et des formulaires](docs/pause-community-beta233.md) : même
ordre avec et sans carte, Communauté (Habitant/Faction/Battle) en haut,
Aventure (Discovery/Progression) au centre et Partie (options/sortie) en bas,
sans barre sous Communauté. Notice Board sans choix de catégorie, notices
uniformes, demandes et récompenses sous le texte, puis Publish. Photo et
Choose a screenshot de la Gazette avant le titre. Brouillons et anciennes
catégories conservés ; parcours de publication et essais Vulkan FR/EN réussis.
`check build assemblePack` avec la suite menus : quatre GameTests requis et
contrôles communautaires réussis. [MRpack local beta.233](build/Sanctuary-beta.233.mrpack)
vérifié ; aucune installation personnelle ni mise à jour du canal.
## beta.232 — Session solo et progression
[Réglages et correctifs](docs/solo-gameplay-beta232.md) : fuseau persistant via
`/sanctuary time timezone Europe/Paris` ou `auto`, disponible au propriétaire
solo sans commandes opérateur. Portée de Construction de 3 à 7 blocs, contour
et pose cohérents ; vein mining maintenu enchaînant sur le bloc regardé.
Aucun diamant généré par les coffres de l'île initiale ; inventaires déjà
remplis et terrain conservés. Avant Atlas : Gazette à gauche, huit boutons
centrés, Notice Board à droite, aucune World Map. Build, 26 GameTests requis
et essais client Vulkan FR/EN réussis.
[MRpack local beta.232](build/Sanctuary-beta.232.mrpack) vérifié ; aucune
installation personnelle ni mise à jour du canal.
## beta.231 — Sous-bois pâles et archipel complet
[Correctif et visite](docs/diagonal-understory-beta231.md) : herbes, fleurs,
tapis de mousse et mousse suspendue dans les parties basses du jardin pâle.
Reliefs et autres habitats validés conservés. Cinq GameTests et build complet
réussis ; huit expansions activées autour d’une principale de 1024, sur 0.
[MRpack normal beta.231](build/Sanctuary-beta.231.mrpack) vérifié.
Le joueur choisit une visite immédiate ; prégénération intégrale interrompue
proprement, chargement restant pendant l’exploration. Labo Vulkan 32 chunks
ouvert au centre de l’île principale ; rendu à apprécier.
## beta.230 — Terrasses pâles et roche jaune
[Correctif des diagonales](docs/diagonal-lower-groves-beta230.md) : chênes pâles
sur les sols bas, abrités ou à ciel ouvert, dripstone dans le dessous du NE.
Savane mêlant roche jaune et pierre normale, minerais conservés. Reliefs,
mangroves et plaines validés conservés. Nouveaux profils uniquement.
Neuf GameTests, build complet et essais natifs 0/42 réussis.
[MRpack normal beta.230](build/Sanctuary-beta.230.mrpack) vérifié ; nouveau
labo Vulkan 32 chunks ouvert sur 0. Rendu à apprécier.
## beta.229 — Marais étagé et cavités sèches
[Refonte du Nord-Est](docs/diagonal-wetland-beta229.md) : relief de la savane
amincie, chênes pâles sous les surplombs, marais et lucioles au-dessus. Eau
uniquement dans des cuvettes naturelles fermées. Cavités soufre/dripstone au
Sud-Ouest, sans lush caves ; mangroves SE et plaines NO conservées. Nouveaux
profils uniquement. Huit GameTests, build complet et essais natifs 0/42
réussis. [MRpack normal beta.229](build/Sanctuary-beta.229.mrpack) vérifié ;
nouvelle visite Vulkan 32 chunks ouverte sur 0. Rendu à apprécier.
## beta.228 — Jardin pâle abaissé et fractures anguleuses
[Finition des diagonales](docs/diagonal-shaping-beta228.md) : jardin pâle
nettement abaissé, lacs remis en évidence, formes plus anguleuses dans les
mangroves et sur les bordures des plaines. Centre NO, cavités SE et savane SO
conservés. Nouveaux profils uniquement. Sept GameTests, build complet et essais
natifs sur 0/42 réussis. [MRpack normal beta.228](build/Sanctuary-beta.228.mrpack)
vérifié ; visite Vulkan 32 chunks ouverte sur la graine 0. Rendu à apprécier.
## beta.227 — Berges, dessous et cavités des diagonales
[Réglages après exploration](docs/diagonal-caves-beta227.md) : jardin pâle plus
accidenté avec berges progressives et pourtour fracturé ; dessous désert/savane
aminci en gardant la surface ; grandes régions souterraines de lush caves et
de dripstone caves sous les mangroves ; érosion étendue vers l’intérieur au
Nord-Ouest, avec un centre de plaines conservé. Profils neufs uniquement.
Six GameTests, build complet et activations/reprise sur 0/42 réussis.
[MRpack normal beta.227](build/Sanctuary-beta.227.mrpack) vérifié ; nouvelle visite
Vulkan 32 chunks ouverte sur la graine 0, quatre diagonales prêtes. Rendu à apprécier.
## beta.226 — Quatre reliefs diagonaux distincts
[Refonte des silhouettes](docs/diagonal-relief-beta226.md) : grandes plaines
centrales et bords rongés au Nord-Ouest ; bassins conservés, marais peu profonds
et hauts reliefs émergés au Nord-Est ; désert/savane sur terrain flottant 3D au
Sud-Ouest ; récifs verticaux à mangrove dominante au Sud-Est, avec boue, argile
et calcite. Diamètre 1024 et axes climatiques conservés. Nouveaux mondes
uniquement ; les profils précédents restent disponibles pour leurs sauvegardes.
Six GameTests, build complet et activations/reprise sur 0/42 réussis.
[MRpack normal beta.226](build/Sanctuary-beta.226.mrpack) vérifié ; visite Vulkan
32 chunks ouverte sur la graine 0, quatre diagonales prêtes. Rendu à apprécier.
## beta.225 — Diagonales climatiques et cannes à sucre
[Première génération des quatre diagonales](docs/diagonal-islands-beta225.md),
chacune de **1024 blocs** : Nord-Est humide avec jardin pâle et marais,
Nord-Ouest frais avec meadow et taïga, Sud-Est tropical avec mangroves et jungles,
Sud-Ouest sec avec désert et savane. Cannes à sucre sur les berges existantes
de Sanctuary Island. Nouveaux profils Petit/Moyen/Grand uniquement ;
les quatre cardinales validées et les anciens profils sont conservés.
Six GameTests, build et activations/reprise sur les graines 42/0 réussis.
Le [MRpack normal](build/Sanctuary-beta.225.mrpack) contient ces ajouts sans Test ;
le rendu reste à apprécier en jeu.
## beta.224 — Stries du cratère Est
[Finition du volcan](docs/east-crater-beta224.md) : relief 223 conservé, lignes
concentriques et hachures verticales, obsidienne étendue et fissures de magma.
Plantation des arbres géants limitée au sol naturel. Sept GameTests ciblés,
build et essais natifs 42/0 réussis ; [MRpack normal](build/Sanctuary-beta.224.mrpack)
vérifié. Nouvelle visite Vulkan 32 chunks ouverte ; rendu à apprécier en jeu.
## beta.223 — Volcan flottant de l’Est
[Premier volcan et jungles](docs/east-volcano-beta223.md) : cône asymétrique,
cratère de lave fermé, ravines ramifiées, jungles ouvertes/denses et bambou,
grands arbres natifs, pandas et temple de jungle avec pièges et butin.
Expansion Est 1024, nouveaux profils uniquement. Build, sept GameTests ciblés
et essais natifs 42/0 réussis ; [MRpack normal](build/Sanctuary-beta.223.mrpack)
vérifié. Visite Vulkan 32 chunks ouverte ; rendu à apprécier en jeu.
Nord, Sud, Ouest et île principale conservés.
## beta.222 — Roche jaune et déclinaisons
[Famille de construction](docs/yellow-rock-beta222.md) : nouveau nom Roche jaune,
escaliers et dalles, recettes natives à l’établi et au tailleur de pierre.
Bloc dans Naturel, famille dans Construction à côté de Stone. Génération 221
conservée ; cinq GameTests et 87 états de modèles sous Vulkan vérifiés.
[MRpack normal](build/Sanctuary-beta.222.mrpack) prêt.
## beta.221 — Pierre jaune de l’Ouest
[Finition des parois](docs/west-yellow-stone-beta221.md) : arbres écorcés et
passe de ravins retirés, relief 3D conservé avec un Voronoi discret, sols plus
cohérents. Nouvelle pierre jaune mineable, juste après Stone dans Naturel et
Construction. Build, six GameTests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.221.mrpack) vérifié. Nouveaux profils uniquement.
## beta.220 — Couronne vivante de l’Ouest
[Berges et reliefs](docs/west-living-rim-beta220.md) : plage libre de 12–15 blocs,
palmiers en retrait, bosquets fleuris, cactus, bois mort et herbes sèches.
Reliefs, ravins et cavités 3D sur le pourtour ; océan, monument et épaves
conservés. Build, cinq GameTests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.220.mrpack) vérifié. Nouveaux profils uniquement.
## beta.219 — Plages basses de l’Ouest
[Finition des berges](docs/west-beaches-beta219.md) : sable au niveau de l’eau
sur environ quatre blocs, montée douce et palmiers denses sur la rive intérieure.
Soufre et cinabre retirés de cette couronne. Océan, temple et épaves 218 conservés.
Six GameTests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.219.mrpack) vérifié. Nouveaux profils uniquement.
## beta.218 — Falaises sous-marines et oasis de l’Ouest
[Nouvelle cuve océanique](docs/west-oasis-beta218.md) : plateau sous-marin,
fracture centrale avec monument, plages de grès et palmiers, soufre et cinabre,
quatre épaves natives. Racines et calcite retirées de ce nouvel Ouest.
Île principale, Nord, Sud et anciens profils conservés. Sept GameTests et
essais natifs 42/0 réussis ; [MRpack normal](build/Sanctuary-beta.218.mrpack)
vérifié. Nouvelle visite Vulkan 32 chunks ; rendu à apprécier en jeu.
## beta.217 — Mer suspendue de l’Ouest
[Premier océan](docs/west-ocean-beta217.md) : mer de 1024 blocs, cuve rocheuse
fermée, racines ramifiées et bords minéraux déformés par bruit 3D. Monument
océanique obligatoire, coraux et relais surélevé. Nord, Sud et île principale
conservés. Build, cinq GameTests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.217.mrpack) vérifié. Nouvelle visite
Vulkan à 32 chunks ouverte ; rendu à apprécier en jeu.
## beta.216 — Forêts et pièges du Sud
[Dernière passe du Sud](docs/south-forest-beta216.md) : forêts denses de chênes
sur la terre, sentiers de surface, pyramide sans torches et plaques piégées
dans les salles et couloirs. Les 14 teintes et le relief sont conservés.
Huit GameTests ciblés, assemblages et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.216.mrpack) vérifié. Nouvelle visite
Vulkan 32 chunks ouverte. La graine 0 reste sans pyramide faute de site enterré.
## beta.215 — Relais régionaux
[Monuments du Nord et du Sud](docs/regional-relays-beta215.md) : relais
surélevés, socles massifs et matériaux locaux ; rosace entièrement colorée.
Village Far West retiré des nouveaux profils, pyramide extérieure agrandie
à 31 × 31, intérieur conservé. Contrôles purs, dix GameTests ciblés et
essais natifs Sud 42 / Nord 0 réussis. [MRpack normal](build/Sanctuary-beta.215.mrpack)
exporté localement ; nouvelle visite Vulkan à 32 chunks. Rendu à apprécier en jeu.
## beta.214 — Village Far West abandonné
[Village décoratif du Sud](docs/south-western-beta214.md) : sept ou huit maisons
en chêne noir, rue principale, porches, façades ornées, lanternes et traces
d’abandon. Implantation et dimensions varient avec la graine, sur les surfaces
du canyon. Terrain et pyramide 213 conservés ; nouveaux profils uniquement.
Contrôles purs, neuf GameTests ciblés et génération/reprise sur 42/0 réussis.
[MRpack normal](build/Sanctuary-beta.214.mrpack) exporté localement ; nouvelle
visite Vulkan à 32 chunks ouverte. Le rendu reste à apprécier en jeu.
## beta.213 — Pyramide et chambres enterrées
[Correction ciblée du Sud](docs/south-buried-beta213.md) : relief et couleurs
212 conservés, sables de surface et acacias custom retirés. Pyramide camouflée
en terracotta locale, descente vers dix petites salles, couloirs et pièges.
Nouveaux profils uniquement. Contrôles purs, 12 GameTests ciblés et essais natifs
42/0 réussis ; [MRpack normal](build/Sanctuary-beta.213.mrpack) exporté localement.
Rendu à apprécier dans la nouvelle visite Vulkan à 32 chunks.
## beta.212 — Couronnes colorées et complexe du désert
[Évolution du Sud](docs/south-rings-beta212.md) : couleurs radiales visibles,
reliefs de mesa localisés, désert clair, arbres secs et petits arbres feuillus.
Grand complexe de grès avec salles, caches et pièges TNT.
Nouveaux profils uniquement ; Nord et île principale conservés.
Contrôles purs, 12 GameTests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.212.mrpack) exporté localement.
Rendu à apprécier dans la nouvelle visite Vulkan à 32 chunks.
## beta.211 — Sud fracturé et cœur coloré
[Refonte des Badlands](docs/south-shattered-beta211.md) : retour au bruit 3D
natif flottant, fissures sans Voronoi, dessous aminci, bandes vanilla dehors
et huit teintes dans le cœur exposé. Sud 1024, nouveaux profils uniquement.
Contrôles purs, 10 GameTests ciblés et essais natifs 42/0 passés ;
[MRpack normal](build/Sanctuary-beta.211.mrpack) exporté localement.
Le rendu reste à apprécier dans la nouvelle visite Vulkan à 32 chunks.
## beta.210 — teintes et végétation des canyons
[Habillage du Sud](docs/south-palette-beta210.md) : quatre familles terreuses par
massif, strates irrégulières discrètes, sol de vallée continu et végétation sèche
aussi sur les mesas. Relief 209 et Nord 208 conservés. Nouveaux profils uniquement.
Build, 11 tests ciblés et essais natifs 42/0 réussis ;
[MRpack normal](build/Sanctuary-beta.210.mrpack) exporté localement.
Le rendu reste à apprécier dans la nouvelle visite Vulkan à 32 chunks.
## beta.209 — premier Sud multicolore
[Prototype des Badlands](docs/south-badlands-beta209.md) : expansion Sud de 1024 blocs,
réseau de canyons par Voronoi inversé, mesas et strates plissées avec les seize
terracottas. Sables assortis sur les surfaces portantes ; Nord 208 conservé.
Nouveaux profils uniquement. Compilation, contrôles ciblés et essais natifs
sur 42 et 0 passés ; [MRpack normal](build/Sanctuary-beta.209.mrpack) exporté
localement. Le rendu est à apprécier dans la nouvelle visite.
## beta.208 — crêtes et replats glacés du Nord
[Dernière passe du Nord](docs/north-ridges-beta208.md) : sommets anguleux,
entailles et replats localisés avec poches et aiguilles de glace. Taïga,
améthyste, igloos et manoir en épicéa conservés. Nouveaux profils uniquement ;
compilation, tests ciblés et essais natifs sur 42 et 0 passés.
[MRpack normal](build/Sanctuary-beta.208.mrpack) exporté localement.
## beta.207 — paysages et manoir du Nord
[Nouvelle passe du Nord](docs/north-landscape-beta207.md) : neige irrégulière
dans une partie des taïgas, reliefs Voronoi localisés sur les hauts plateaux et
manoir procédural en épicéa. Diamètre, dessous aminci et failles d’améthyste
conservés. Profils neufs uniquement ; contrôles natifs sur les graines 42 et 0
passés. [MRpack normal](build/Sanctuary-beta.207.mrpack) exporté localement.
## beta.206 — cavités froides du Nord
Le [nouveau dessous du Nord](docs/north-cavities-beta206.md) conserve le paysage
de surface de la 205 et amincit sa masse rocheuse avec des épaisseurs variables.
Les cavités accueillent glace, eau dans les creux fermés et failles ouvertes
d’améthyste bordées de calcite et de basalte. Nouveaux mondes uniquement ;
la répartition géographique diorite/granit reste à décider.
Contrôles natifs sur les graines 42 et 0 passés ;
[MRpack normal](build/Sanctuary-beta.206.mrpack) exporté localement.
## beta.205 — Nord en bruit 3D natif
Le [nouveau profil nordique](docs/north-noise-beta205.md) retire l'étirement Peaks
et ses montagnes ajoutées. L'enveloppe grandit autour du bruit natif, puis la
taïga et la glace habillent ses surfaces. La lecture des hauteurs est interpolée
entre les points de mesure pour éviter les carrés de palette. Contrôles et
génération/reprise sur les graines 42 et 0 validés ; rendu à confirmer en visite.
[MRpack normal](build/Sanctuary-beta.205.mrpack) exporté localement.
## beta.204 — Sanctuary autonome et Nord Peaks
Le [chantier d’intégration](docs/base-peaks-beta204.md) transfère Demeure et
le terrain validé dans le mod principal. Sanctuary normal propose l’île récente
en Small, Medium ou Large ; le module Test reste facultatif pour les outils de
laboratoire. Le nouveau Nord reprend Peaks avec sa taïga géante et ses glaces.
Compilation, contrôles ciblés, création des trois tailles sans Test et éditeur
Vulkan FR/EN validés. [MRpack normal](build/Sanctuary-beta.204.mrpack) exporté
localement ; aucun canal public mis à jour. Nouveaux mondes uniquement.
## beta.203 — fractures et taïga géante au Nord
[Dernière passe du Nord](docs/north-fracture-beta203.md) : variations de relief
+2 -2
View File
@@ -52,11 +52,11 @@ 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.
Demeure est embarqué comme mod autonome, sous GPL-3.0-or-later. Son modèle
Demeure est intégré au code de Sanctuary depuis beta.204, sous GPL-3.0-or-later. Son modèle
provient des sources de KOKA99CAB dans `Structures/demeure`, référencées par
l’inventaire 26.2, et a été adapté à Minecraft 26.3-pre-2. Voir
`mods/demeure/PROVENANCE.md` pour les empreintes et les différences du port.
Le JAR Demeure contient sa licence et ce document de provenance.
Le JAR Sanctuary conserve sa licence GPL et `licenses/Demeure-PROVENANCE.md`.
## Inventaire beta.010
+2 -2
View File
@@ -3,8 +3,8 @@ plugins {
id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false
}
tasks.named('build') { dependsOn(':sanctuary:build', ':demeure:build', ':jei:build', ':sanctuary-test:build') }
tasks.named('check') { dependsOn(':sanctuary:check', ':demeure:check', ':jei:check', ':sanctuary-test:check', 'verifyPack') }
tasks.named('build') { dependsOn(':sanctuary:build', ':jei:build', ':sanctuary-test:build') }
tasks.named('check') { dependsOn(':sanctuary:check', ':jei:check', ':sanctuary-test:check', 'verifyPack') }
tasks.register('verifyPack', Exec) {
group = 'verification'
+103
View File
@@ -0,0 +1,103 @@
# UNI-WG-204 — Sanctuary autonome et Nord Peaks
Branche `codex/sanctuary-base-peaks-beta204`, depuis beta.203 (`454d9a5`).
Demande : absorber Demeure dans Sanctuary, rendre le terrain validé du laboratoire
accessible avec le mod normal seul, puis remplacer la nappe nordique par le
générateur Peaks existant en conservant taïga géante, glaces et igloos.
## Contrat
- Demeure interne : même chemin `data/demeure/footprints-v1.json`, schéma 1,
mêmes règles serveur et attribution. Aucun transfert de données.
- Nouveau preset public Sanctuary, trois tailles ; types vanilla conservés.
Introduction normale conservée ; diagnostics et raccourcis de labo facultatifs.
- Codecs et ressources historiques `sanctuary:*` et `sanctuary_test:*` conservés.
Le transfert de module ne renomme pas ces identifiants.
- Nouveau profil 204 uniquement pour le Nord Peaks, diamètre 1024. L'île de
départ reprend les paramètres 203 ; aucun terrain déjà créé régénéré.
- Essais sur mondes de développement neufs et copies de développement seulement,
graines 0, 42 et 4736390610738281858 ; publication/installation non demandée.
## Réalisation
### Mod normal
Demeure n'est plus un JAR Fabric imbriqué : ses quatre classes, son mixin de
placement, ses tests et son attribution sont intégrés à Sanctuary. Le service
s'enregistre une seule fois depuis l'initialisation principale. Un ancien JAR
Demeure séparé est refusé pour éviter deux installations des mêmes hooks.
Le terrain validé restait dans `sanctuary-test` ; le preset public du mod normal
ne pointait donc pas vers ce terrain. Le runtime de l'île, ses ressources,
ses mixins et son écran de taille sont désormais dans Sanctuary : biomes,
grottes, minerais, bassins, mines, ruines, ancres, rosace, station et expansions.
Les variantes historiques restent enregistrées pour lire leurs identifiants,
mais une seule entrée Sanctuary apparaît dans le menu, à côté des types vanilla.
Small/Medium/Large sélectionnent les nouveaux paramètres `sanctuary:island204_*`.
Le module Test conserve uniquement ses outils : diagnostics, visites guidées de
développement, monde plat, commandes et raccourcis d'essai. Il ne fournit plus
de mixin ou de ressource indispensable au monde normal. Le nom du package Java
`fr.koka.sanctuarytest` et les anciennes clés `sanctuary_test:*` restent inchangés
dans le runtime déplacé afin de limiter la portée du transfert.
### Nord
Pour les nouveaux profils 204, le Nord 1024 utilise directement la densité Peaks
du moteur d'expansion, avec son épaisseur et son amplitude verticales normales.
La coque de plateaux `NorthShape203` n'intervient plus. L'écologie s'appuie sur
des colonnes mesurées dans cette densité, mises en cache sur une grille de huit
blocs : vallées, taïga, vieux épicéas, roche, neige, pics et glaciers dépendent
du relief. Les épicéas géants 203 et les formations de glace vanilla sont repris.
La glace des cavernes reste conditionnée à une couverture rocheuse suffisante.
Les cinq igloos cherchent des épaules enneigées suffisamment soutenues, sans
aplatir toute la montagne ; un seul possède le laboratoire de guérison. L'arrivée
et le relais évitent les bâtiments. Les deux bassins imposés dans les anciens
profils nordiques ne sont pas transposés : ils reformaient des plateformes dans
le nouveau relief. Les autres directions et les profils 200–203 ne changent pas.
## Vérifications — 4/5 octobre 2026
- `./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=base204,demeure,menus,operator,realtime
-PsanctuaryAtlasOnly=true` : succès, tests de logique et 13 GameTests natifs
ciblés. Ces GameTests vérifient notamment l'absence de Test et de Demeure
séparé, les presets publics/historiques, les empreintes Demeure et l'amplitude
Peaks réservée au Nord 204. La suite native historique entière n'a pas été rejouée.
- `:sanctuary:runClientGameTest -PsanctuaryClientTests=true
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryShared196ClientTests=true` :
`SHARED196_CLIENT_PASS`, sans Test chargé. FR/EN, tailles, Annuler/Échap,
relecture des paramètres sauvegardés et éditeur Plat vanilla vérifiés.
- Création sur le classpath de production sans Test, heap 2048 Mio : Small
graine 42, Medium graine 0 et Large graine 4736390610738281858. Les trois
serveurs atteignent l'état prêt et placent le spawn sur l'île. Journaux dans
`build/base204-standalone/`. Ce ne sont pas des mesures comparatives de performance.
- Nord natif Small 42, profil 204 : offrande réelle, rejet des mauvaises offres
et doublons, interruption puis reprise du journal, arrivée/relais et cinq
igloos dont une cave vérifiés. Centre de l'expansion `(-128, -944)` ; arrivée
`(-112, 308, -1056)`. L'annonce SGA est émise après préparation.
- Échantillonnage du Nord : sommets Y=103–455, épaisseur maximale 381 blocs ;
prairie, taïga, forêt ancienne, neige, pics, glacier et roche présents.
Dans les chunks inspectés autour de `(-256, 283, -848)`, le plus grand tronc
mesure 60 blocs. Glace et air vérifiés dans une caverne ; glacier vérifié
autour de `(-80, 303, -1120)`. Reçus `build/worldgen-lab/north204-a/small/42/`.
- MRpack normal `Sanctuary-beta.204.mrpack` exporté et ZIP vérifié : Minecraft
26.3, Loader 0.19.5, Fabric API 0.160.5+26.3 ; ni JAR Test ni Demeure séparé.
SHA-256 : `8c90090a784bd7c799849e916be2e3e36214a3dbf6ce8b4925d31a9fca8c5086`.
## Limites et livraison
Les essais natifs du nouveau Nord couvrent la graine 42. Son aspect reste à
évaluer en visite, et la recherche d'un village n'a pas été vérifiée sur toutes
les graines. Ces résultats macOS/Vulkan ne constituent pas une validation Windows.
L'hydrologie du Nord reste à reprendre sur son relief Peaks si l'on souhaite
rétablir de grands lacs. Aucun ancien monde, chunk ou format de sauvegarde n'a
été migré ; les nouvelles formes nécessitent un nouveau profil 204.
Artefact local dans `build/` et copie dans `~/Downloads/`. Une copie indépendante
du monde de test est ouverte pour la visite `north204-visit`, en créatif,
commandes autorisées et vue 32 chunks (`NORTH204_VISIT_OPEN`, Vulkan confirmé).
Aucun packwiz public, serveur personnel
ou instance Prism n'a été mis à jour par ce chantier.
+114
View File
@@ -0,0 +1,114 @@
# Diagonales : transitions, dessous et cavités — beta.227
Retour R045 sur beta.226, 5 octobre 2026. Branche `codex/diagonal-caves-beta227`,
base `5fb1407`. Anciennes générations conservées ; nouveaux profils 227 uniquement.
- NE jardin pâle/marais : transitions d’étangs sans paliers brutaux, relief 3D
plus présent à l’intérieur, fragmentation prononcée du pourtour, petites
échappées d’eau admises mais pas de rideau d’eau massif.
- SO : surface désert/savane appréciée ; retirer de l’épaisseur rocheuse inutile
sous le plateau sans refaire sa surface.
- SE : mangroves et silhouette validées ; ajouter de grandes régions de lush
caves et dripstone caves dans les parties inférieures.
- NO : grandes plaines validées ; commencer le grignotage 3D plus près du centre.
Captures consultées dans le labo 226 : `2026-10-05_21.05.12.png` et
`2026-10-05_21.07.21.png`. Elles montrent des fonds d’étangs à transitions nettes.
La profondeur binaire et le masque de protection des bassins dans 226 sont
remplacés par des transitions continues. La photo seule ne prouve pas un
problème de frontière de chunk ; les contrôles ci-dessous portent sur les
transitions du champ et la décoration native.
## Réalisation
Les champs 225/226 restent figés. 227 s’appuie sur le champ 226 pour conserver
la surface méridionale et la forme des récifs de mangrove. Aucun changement
de sauvegarde, de génération existante ou de dimension.
NE : retrait du halo binaire autour des lacs ; profondeur interpolée, relief
intérieur continu avant arrondi en blocs, rugosité 3D atténuée près de l’eau.
La bordure entière peut être découpée, y compris les anciens abords protégés.
L’eau n’est placée que sur les fonds encore présents ; pas de remplissage des
grandes failles ouvertes. De petites échappées restent possibles. Il ne s’agit
pas d’une simulation d’hydrologie ni d’un réseau de cascades.
SO : amincissement variable, environ 40 à 64 blocs sous le toit avec irrégularité
3D du dessous. Les 24 blocs supérieurs restent sur le champ accepté. Sur les
grilles de sondage 0/42, environ 49 à 51 % du volume rocheux échantillonné est
retiré ; ce n’est pas un recensement exhaustif de toute l’île.
SE : densité et biomes de surface 226 conservés. Sous la surface, de larges
régions de grottes luxuriantes et à concrétions reçoivent les features natives
26.3 (mousse, lianes lumineuses, fleurs sporifères, argile, grandes concrétions,
stalactites/stalagmites). Plages de placement adaptées à Y=40–320. Aucune
nouvelle lave, forêt d’azalées de surface ou nouvelle distribution de minerais
ajoutée par ces deux biomes de cavités. Les volumes existants servent de support,
sans nouvelle excavation de la silhouette validée.
NO : le masque peut maintenant agir jusqu’à 340 blocs de distance interne
au bord (contre 180), sans modifier le noyau de plaines restant. Le champ
supplémentaire ne fait que retirer de la matière.
## Vérifications
Six GameTests ciblés réussis : transitions NE et relief intérieur sur quatre
graines, extension des découpes NO et centre conservé, surface SO intacte et
volume réduit, champ SE identique et deux vastes biomes souterrains, trois
presets publics, ordre des features, déterminisme après recompilation et
anciens profils lisibles.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal227 -PsanctuaryAtlasOnly=true` réussit
en 4 min 2 s (`build/diagonal227-build.log`).
Les premiers essais natifs 0/42 valident le NE, les arbres immergés et les
décorations des deux types de caves au SE. Ils s’arrêtent sur un contrôle trop
étroit des arbres de savane : un seul chunk de chaque biome, tous sans arbre
complet. Lecture des chunks FULL sauvegardés : 11 troncs / 70 feuilles sur 0,
24 troncs / 214 feuilles sur 42. L’instrumentation élargit désormais la recherche
à d’autres chunks de savane, sans changer la production. Helper recompilé et
exporté ; nouveaux essais isolés `diagonal227-final0` et `diagonal227-final42`
réussis, processus terminés normalement. Les mondes du premier essai restent
intacts. Le helper final est recompilé puis exécuté dans ces deux essais ;
aucun changement de production après le build complet.
Les quatre offres réelles, leur coût, le refus d’un double paiement, la reprise
après interruption et les quatre relais utilisables sont vérifiés sur chaque
graine. Chaque activation prépare 49 chunks FULL. Les arbres natifs sont
présents dans les quatre expansions ; 3 et 7 troncs enracinés dans les marais
peu profonds ont été observés. Les sondages des cavités SE trouvent respectivement
120/171 blocs de décoration lush et 34/30 de pointed dripstone. Ces comptages
prouvent la décoration native dans les chunks examinés, pas sa densité globale.
Les cannes à sucre de l’île principale restent présentes : 42 pieds sur 0,
94 sur 42. Rapports ignorés sous `build/worldgen-lab/diagonal227-final{0,42}`.
## Distribution et visite
MRpack normal : `build/Sanctuary-beta.227.mrpack`, copie dans Downloads.
Minecraft 26.3, Fabric Loader 0.19.5. Archive contrôlée, JAR conforme au build,
trois tailles présentes, aucun module Test ni monde inclus. Les 398 ressources
historiques de worldgen comparées sont identiques à 226 ; seul l’alias public
Sanctuary pointe désormais vers les nouveaux profils.
- SHA-256 MRpack : `5ffa88d0a8d30b17276bda0e644e3406dd2a2bdc7727a27e546320a502adf547`.
- SHA-256 JAR : `e759515e49cd254f41e9ab786478cba02613c560534f77d89b820f69ae384dee`.
Nouvelle visite isolée `diagonal227-final-visit`, monde
`Sanctuary-Diagonal-227-0` : copie du nouveau monde de vérification arrêté,
graine 0, île principale Petit, quatre expansions de 1024 prêtes, créatif avec
commandes, distance 32 chunks et simulation 5. Départ au-dessus du relais NE.
Ouverture confirmée le 5 octobre à 21:33:59 : `DIAGONAL227_VISIT_OPEN`,
position `848, 384, -496`, backend Vulkan confirmé dans
`build/diagonal227-solo.log`. Aucune validation OpenGL.
| Région | Relais X, Y, Z sur la graine 0 |
| --- | --- |
| NE jardin pâle/marais | 848, 336, -496 |
| SE mangroves/cavités | 560, 377, 816 |
| SO savane/désert | -832, 217, 512 |
| NO plaines/taïga | -608, 238, -768 |
Le rendu reste à apprécier par le joueur, notamment les berges très découpées
et l’étendue visible des cavités. Pas de visite complète Moyen/Grand, de mesure
de fluidité ni d’essai Windows revendiqués. Aucun ancien monde modifié, aucun
canal packwiz avancé et aucune installation Prism synchronisée.
+100
View File
@@ -0,0 +1,100 @@
# Diagonales climatiques et cannes à sucre — beta.225
## Contrat
Demande du 5 octobre 2026 : cannes à sucre près de l’eau de Sanctuary Island,
puis quatre expansions diagonales de **1024 blocs** (512 remplacé pendant
le chantier à la demande du joueur). Nord froid, Sud chaud,
Est humide, Ouest sec. Branche `codex/diagonal-islands-beta225`, base `1388d8a`.
- Nord-Est : marais frais, jardin pâle, forêt sombre et taïga.
- Nord-Ouest : meadow, prairies, taïga et bosquets froids.
- Sud-Est : mangroves, jungle et bambou.
- Sud-Ouest : désert, savane et prairies sèches.
Première variante : surfaces étendues, collines, dessous flottant irrégulier,
zones humides fermées dans les dépressions du terrain. Végétation et faune
des biomes natifs 26.3. Relais de continuation conservé. Les quatre cardinales
restent en 1024 avec leur génération validée. Pas de nouvelle structure ici.
Nouveaux profils 225 seulement, dans les trois tailles Sanctuary. Les profils
224 et antérieurs gardent leurs règles et leurs tailles d’expansion. Aucune migration ou régénération de sauvegarde.
Les essais utilisent uniquement des mondes neufs de développement (42 et 0).
Le message SGA et l’aide de visite réservée aux opérateurs sont conservés.
## Implémentation
Champ de relief déterministe, collines à grande échelle, bordure irrégulière et
bruit 3D dans le dessous. Épaisseur variable, de neuf blocs sur les extrémités
à 68 blocs à l’intérieur. Les dépressions humides ont un niveau commun Y=204,
un fond protégé et une couronne de terrain ferme ; aucune simulation
hydrologique supplémentaire. La sélection des biomes est continue dans les
coordonnées du monde et utilise les végétations natives 26.3.
Les offres des ancres diagonales demandent un diamètre 1024 et conservent
l’éventail de placement existant. Préparation limitée à 49 chunks autour du
relais ; le reste se génère à l’exploration. Relais simple de continuation pour
cette première variante. Les monuments régionaux des cardinales sont conservés.
Les cannes réutilisent le plan des berges de surface, après leur aménagement.
Groupes de deux ou trois blocs, sur sol accepté par Minecraft et à côté d’une
eau réellement présente. Écriture limitée au chunk décoré, sans dépendre de
l’ordre de décoration des chunks voisins. Aucun nouveau bassin créé.
## Vérifications
Six GameTests ciblés (`base204,diagonal225`) réussis : trois tailles publiques
sans Test, lecture des anciens profils, biomes et étanchéité sur quatre graines,
reproductibilité aux limites des chunks, éventail directionnel et absence de
collision, conservation des matériaux du volcan, règles de survie des cannes.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal225 -PsanctuaryAtlasOnly=true`
réussit en 3 min 57 s. Log : `build/diagonal225-build.log`.
L’essai natif 42 passe les quatre offres, le paiement, l’interruption/reprise,
les relais et la végétation des quatre directions. 94 pieds de canne observés
sur les berges de l’île principale. Le premier essai 0 a révélé une erreur
**du contrôle** : son premier échantillon de mangrove était sur la berge sèche,
ce qui ne prouvait rien sur l’eau de la dépression. Le contrôle cherche désormais
une colonne réellement immergée et vérifie son fluide natif. Le nouvel essai 0 passe aussi les quatre activations et les contrôles natifs ;
43 pieds de canne observés. La correction ne touche que l’instrumentation du
module Test, recompilée et réassemblée ensuite ; le terrain livré est identique.
| Graine | Nord-Est X/Z | Sud-Est X/Z | Sud-Ouest X/Z | Nord-Ouest X/Z | Cannes |
| --- | --- | --- | --- | --- | --- |
| 42 | 544 / -816 | 816 / 544 | -816 / 512 | -704 / -736 | 94 |
| 0 | 848 / -496 | 560 / 816 | -832 / 512 | -608 / -768 | 43 |
Les huit expansions mesurent 1024 blocs et passent chacune par 49 chunks FULL
préparés. Présence de bois/feuilles natifs dans les quatre directions, mangroves
et bambou à l’Est, cactus dans le désert à l’Ouest. Ces relevés sont locaux,
pas un inventaire exhaustif de chaque île. Reçus ignorés :
`build/worldgen-lab/diagonal225-check42/small/42/{cold,warm}.json` et
`build/worldgen-lab/diagonal225-wet-check0/small/0/{cold,warm}.json`.
## Distribution et limites
MRpack standard vérifié : archive intègre, JAR exact, dépendances 26.3/Loader
0.19.5, trois tailles 225 et 382 ressources historiques de génération identiques
à beta.224 (sauf alias public du preset). Aucun monde ni module Test inclus.
SHA-256 : `b178c15d8a40a1627134c64b343d13d508f9e7c95f85d43a1a88c8769d8e8c0c`.
Le rendu reste à apprécier en jeu. Pas de campagne graphique ou Windows,
ni de nouvelle exploration complète Medium/Large. Pas de changement du canal
packwiz ni de l’installation Prism personnelle. Les mondes de contrôle restent
dans les dossiers de développement ignorés.
## Visite du 5 octobre 2026
À la demande du joueur, nouveau solo `Sanctuary-Diagonal-225-0`, graine 0,
copie du serveur de contrôle arrêté. Les quatre diagonales sont déjà prêtes ;
départ au Nord-Est en 848 / 254 / -496. Vulkan, rendu 32 chunks, simulation 5,
créatif/vol et commandes autorisées. Le client confirme `DIAGONAL225_VISIT_OPEN`.
Ajout du nom Diagonal-225 à la liste du lanceur de visites et à son aide client
optionnelle, compilée avec `:sanctuary-test:exportDuoLaunch`. L’identité des
sources du contrôle a été vérifiée avant ce seul changement de lanceur ;
génération et ressources de production identiques. Les anciens mondes et le
MRpack beta.225 livré restent inchangés. La visite est une copie neuve isolée.
+64
View File
@@ -0,0 +1,64 @@
# Jardins pâles bas et roche jaune — beta.230
Retour R048 sur beta.229, 5 octobre 2026. Base `55df522`, branche
`codex/lower-groves-beta230`. Nouveaux profils uniquement, aucune migration
ou régénération des anciennes cartes.
NE : garder le relief validé et étendre les chênes pâles aux terrasses situées
sous le niveau du plateau, y compris à ciel ouvert. Dripstone dans les parties
inférieures ; marais et lucioles restent sur le plateau. Eau toujours
facultative et conditionnée à une cuvette naturellement fermée.
SO : mêler la roche jaune existante à la pierre normale en masses continues,
en conservant les minerais natifs et les cavités soufre/dripstone.
SE mangroves et NO plaines validés par le joueur, champs de relief conservés.
Le seuil inférieur suit le plafond ondulé du terrain savane (Y=230 ± 18),
avec une bande supérieure de 12 blocs laissée au marais. Il ne suit plus le
sommet local de chaque colonne : une terrasse basse ouverte est désormais un
sol de jardin pâle. Même support 2 × 2 et même dégagement pour les arbres.
Les formations natives de dripstone sont ajoutées au biome pâle inférieur.
Les buissons à lucioles restent exclusivement au-dessus de ce seuil.
La roche jaune est répartie par un bruit volumique lent, sans changer la
forme de la savane. La substitution après décoration ne vise que Stone ;
elle conserve les blocs de minerai, leur voisinage immédiat dans le chunk,
les autres roches, le soufre et le cinabre. Aucune variante de minerai jaune
ajoutée et aucun changement des tags minéraux globaux.
Neuf GameTests ciblés réussis : reliefs conservés, séparation des habitats,
terrasses ouvertes et sols abrités, cuvettes fermées, cavités sèches SO,
substitution réelle de pierre avec conservation des minerais, presets et
ordre des features. Les grilles 0/42 trouvent 274/285 sites de chêne pâle,
dont 216/245 ouverts ; ces chiffres ne sont pas un inventaire de l’île.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal230 -PsanctuaryAtlasOnly=true` réussit
en 2 min 38 s. Contrôles natifs réussis sur 0/42 : quatre activations réelles, paiements et
refus des doubles offres, interruption/reprise, 49 chunks FULL par expansion,
relais utilisables. Les nouveaux mondes de contrôle sont arrêtés normalement.
Au NE, dans les chunks sondés : 38/32 blocs de tronc pâle, quatre colonnes
de racine à ciel ouvert sur chaque graine (un tronc de 2 × 2), 103/76 blocs
de pointed dripstone et 7/1 buissons à lucioles supérieurs. Au SO : les deux
roches sont présentes (462/1711 blocs jaunes et 7180/2707 Stone dans les
échantillons), avec pics de soufre et dripstone. Ces comptes attestent la
présence, sans mesurer la proportion globale sur chaque île.
Rendu à apprécier en jeu.
MRpack normal vérifié, copie dans Downloads : Minecraft 26.3 / Fabric Loader
0.19.5, trois tailles, JAR identique au build, aucun module Test ni monde.
Les 432 ressources historiques de worldgen comparées à 229 sont identiques.
SHA-256 pack : `b080b17d7e54427d3a6b89da2b009637e99a648752ba49068a274fa35499f8e4`.
SHA-256 JAR : `c72cc0afea27e7d71c73d6f4edc4f874bc5fb665288eba243d7faafcba5e3276`.
Reçu : `build/diagonal230-mrpack-receipt.json`.
Aucune publication de canal ni synchronisation Prism, aucune validation
Windows ou mesure de fluidité.
Nouvelle visite isolée `diagonal230-visit`, monde `Sanctuary-Diagonal-230-0`,
copiée depuis le contrôle graine 0 arrêté. Île principale Petit et quatre
expansions 1024 prêtes. Créatif avec commandes, 32 chunks, simulation 5 ;
départ au relais NE (856, 273, -496). Ouverture Vulkan confirmée à 22:45:00
le 5 octobre 2026, `DIAGONAL230_VISIT_OPEN` dans `build/diagonal230-solo.log`.
Le rendu reste à apprécier par le joueur ; anciennes visites conservées.
+110
View File
@@ -0,0 +1,110 @@
# Reliefs distincts des diagonales — beta.226
Retour du 5 octobre 2026 sur beta.225. Biomes appréciés, silhouettes refusées :
grands plateaux identiques, bords tranchés. Branche `codex/diagonal-relief-beta226`,
base `e2b9ee7` et conservation des ajustements locaux du lanceur de visite 225.
- Nord-Est : garder les bassins, davantage de hauts reliefs émergés sans excaver
leur intérieur ; marais moins profonds, arbres plus fréquents dans l’eau,
quelques grands lacs conservés.
- Nord-Ouest : grandes plaines et végétation validées ; bords rongés fortement
en 3D, effet qui diminue en allant vers le centre, centre préservé.
- Sud-Ouest : remplacer la base par un terrain flottant 3D natif, grandes
surfaces de sable et roche, plateaux de savane, léger rehaussement local.
- Sud-Est : relief très fragmenté, hauts et bas, récifs verticaux, davantage
de mangroves ; boue, argile et calcite. Abandon de l’ambiance plate.
Diamètre 1024, Est humide/Ouest sec. Nouveaux profils 226 seulement. Île
principale, cannes à sucre et quatre cardinales conservées. Pas de migration
des mondes 225 ou antérieurs ; nouvelles visites isolées pour les essais.
## Réalisation
La révision 225 reste figée. Trois nouveaux profils 226 réutilisent exactement
la génération de Sanctuary Island ; seules les diagonales passent au nouveau
champ. Les anciennes ressources et leurs identifiants restent lisibles.
Le Nord-Ouest réemploie les colonnes et matériaux 225 à plus de 180 blocs de
la bordure. À l’extérieur, deux échelles de bruit volumique découpent le bord
et le dessous ; un masque continu réduit l’effet en approchant du centre.
Le Nord-Est conserve les dépressions et leur enveloppe étanche, garde une
partie des lacs profonds et relève les autres fonds à une ou deux couches
d’eau. Des reliefs émergés ajoutés en 3D restent à distance des bassins. Les
chênes de marais natifs reçoivent davantage de tentatives de plantation.
Le Sud-Ouest repart du champ flottant `PopulationIslandDensity`, sans le
plateau commun 225, avec une limite haute légèrement ondulée pour les savanes.
Le Sud-Est étire verticalement ce champ, ajoute des fractures et des récifs
volumiques, et favorise les mangroves natives. Les différentes corniches
exposées reçoivent du sol, pas seulement le sommet de la colonne ; les veines
cohérentes de calcite et d’argile traversent la roche. Le sable repose sur une
base solide. Les biomes de végétation natifs restent ordonnés et compatibles.
Les bassins 225 conservés concernent le Nord-Est. Le Sud-Est abandonne ses
anciens bassins plats ; ses mangroves poussent sur les corniches de boue.
La préparation d’une expansion reste limitée à 49 chunks autour du relais.
Pas de passe d’hydrologie ou de simulation supplémentaire, ni de nouvelle
structure. Relais cherché sur un support réel ; le petit socle 3 × 3 existant est conservé,
sans grande terrasse artificielle.
## Contrôles
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal226 -PsanctuaryAtlasOnly=true` réussit
en 2 min 42 s (`build/diagonal226-build.log`). Six GameTests : trois profils
publics et historiques, conservation du centre NO, bords rongés, bassins NE
étanches et moins profonds sur quatre graines (0, 42, 2026, -9137), volumes
natifs méridionaux, part des mangroves, déterminisme après recompilation,
relais supportés, ordre des features et routage des cardinales validées.
Sur la grille de contrôle des deux graines 0/42, le NO perd plus de la moitié
de ses colonnes de bord à leur ancienne hauteur, avec un centre strictement
conservé. Le NE conserve des lacs profonds et 56 à 84 % de colonnes humides
peu profondes sur les quatre graines. Les reliefs ajoutés dépassent localement
85 blocs. Au SE, la mangrove couvre 67 à 73 % des colonnes émergées échantillonnées
sur les graines de champ 0/42 ; les surfaces vont localement de Y=63 à Y=439.
Ce sont des sondages de champs, pas des mesures exhaustives des mondes.
Deux mondes natifs neufs, graines **0 et 42**, passent les quatre offres réelles,
le refus des paiements incorrects et doubles offres, l’interruption après
réservation, la reprise, les relais utilisables et la décoration native.
Chaque préparation reste à 49 chunks FULL. Trois troncs natifs enracinés
sous le niveau de l’eau ont été constatés dans le marais de chaque monde
(arrêt du contrôle dès trois arbres). Bois et feuilles présents sur les quatre
îles, mangroves sur les corniches SE. Cannes de l’île principale : 42 pieds
sur 0, 94 sur 42. Les relevés de plantes portent sur quelques chunks par biome ;
un échantillon désert sans cactus ne constitue pas un inventaire de l’île.
Reçus ignorés :
`build/worldgen-lab/diagonal226-check0/small/0/{cold,warm}.json` et
`build/worldgen-lab/diagonal226-check42/small/42/{cold,warm}.json`.
## Distribution
MRpack standard beta.226 dans `build/` et `~/Downloads/`, contrôlé après
assemblage : archive intègre, JAR construit exact, Minecraft 26.3 / Loader
0.19.5, trois nouveaux profils et 388 ressources historiques de génération
identiques à beta.225 (hors alias public). Aucun monde ou module Test inclus.
SHA-256 : `c1ce5fc71a3e0417194d591d1ddd04b00dcef46565dfdb34ccd7ee2a01217e02`.
Pas de publication du canal packwiz, de push ou de modification de Prism.
Les mondes existants restent sur leur révision. Rendu à apprécier par le joueur ;
pas de campagne de screenshots, de validation Windows ou d’exploration complète
Medium/Large. Les contrôles des trois tailles vérifient leurs registres et presets.
## Visite
Copie isolée du serveur 0 arrêté dans
`build/worldgen-lab/diagonal226-visit/small/0/client/saves/Sanctuary-Diagonal-226-0`.
Créatif, vol et commandes autorisés ; 32 chunks de rendu, simulation 5,
mémoire 4 Gio. Départ NE à 848 / 270 / -496. Vulkan confirmé ; `DIAGONAL226_VISIT_OPEN` le 5 octobre à 20:32:59.
Le joueur passe en Spectateur à 20:33:04.
Points d’observation libres, téléportation possible :
- NE : `/tp @s 848 270 -496`
- SE : `/tp @s 560 425 816`
- SO : `/tp @s -832 265 512`
- NO : `/tp @s -608 286 -768`
+88
View File
@@ -0,0 +1,88 @@
# Diagonales : abaisser et ciseler — beta.228
Retour R046 sur beta.227, 5 octobre 2026. Base `70a34df`, branche
`codex/diagonal-shaping-beta228`. Nouveaux profils uniquement.
Le jardin pâle monte trop haut entre les étangs. Conserver son pourtour 3D,
abaisser fortement le relief et rendre les lacs lisibles. Les mangroves et
leurs cavités conviennent, mais leurs volumes demandent des arêtes moins
rondes. Même finition sur les bordures des plaines NO, plateau général
conservé. L’épaisseur et la surface désert/savane SO sont validées et figées.
Captures consultées dans le labo 227 : `2026-10-05_21.36.56.png` et
`2026-10-05_21.40.07.png` ; grandes buttes NE et découpe arrondie NO visibles.
## Réalisation
NE : comprimer les hauteurs émergées, réduire la surélévation et l’annuler sur
les fonds aquatiques. SE/NO : fractures Voronoi à largeur modulée par du bruit
3D ; calcul cellulaire mis en cache par colonne. Le noyau de plaines NO et le
champ SO restent ceux de 227. Réutiliser les deux biomes de cavités 227 au SE.
La partie émergée du NE est comprimée à 55 % de sa hauteur au-dessus de l’eau.
Les deux surélévations passent de 120 + 100 à 12 + 8 blocs, avec une transition
continue qui les annule sur les fonds aquatiques. Le masque 3D du bord reste
celui de 227. L’eau garde son niveau Y=204, sans nouvelle simulation hydrologique.
Le Voronoi SE/NO fournit la distance aux faces des cellules ; deux bruits 3D
modulent la largeur des fractures à différentes échelles. Le calcul cellulaire
est réutilisé sur la colonne entière. Au NO, l’action supplémentaire cesse
à 280 blocs de distance interne au bord : le centre est exactement celui de
227. Au SE, seules des découpes sont ajoutées ; les cavités existantes ne sont
pas remplies. Les nouveaux rebords reçoivent les matériaux du terrain 226.
## Vérifications
Sept GameTests ciblés réussis (`build/diagonal228-tests.log`). Sur les grilles
NE de quatre graines, dont les deux graines d’expansion réellement visitées :
sommets entre Y=228 et 237, baisse moyenne intérieure de 30 à 39 blocs,
2,4 à 2,6 fois plus de colonnes en eau qu’en 227. Plus de 95 % des anciens fonds
aquatiques 225 du centre sont de nouveau en eau. Ce sont des échantillons,
pas une mesure exhaustive de la superficie des lacs.
Sur 0/42, environ 8–10 % de matière en moins au NO et 22 % au SE dans les
grilles sondées ; centre NO conservé, deux biomes de caves présents, relais
sur support, surfaces de mangrove préservées comme habitats. Le champ de
densité et les matériaux SO sont identiques à 227 sur les échantillons.
Déterminisme, trois presets publics et anciens identifiants contrôlés.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal228 -PsanctuaryAtlasOnly=true` réussit
en 2 min 43 s (`build/diagonal228-build.log`), avec les sept GameTests.
Contrôles natifs réussis sur deux nouveaux mondes isolés, graines 0/42 :
`diagonal228-check0` et `diagonal228-check42`. Paiements et refus des doubles
offres, interruption/reprise, quatre expansions de 1024, 49 chunks FULL par
activation et relais utilisables vérifiés. Les sondages trouvent des arbres
dans chaque région, ainsi que des troncs enracinés dans l’eau au NE (3/7).
Au SE, présence native des décorations lush (389/114 blocs échantillonnés) et
pointed dripstone (38/76). Ces comptages ne mesurent pas leur abondance globale.
Les deux processus se terminent normalement. Aucun ancien monde modifié.
## Artefact local
MRpack normal `build/Sanctuary-beta.228.mrpack`, copié dans Downloads.
Archive et JAR vérifiés, Minecraft 26.3 / Fabric Loader 0.19.5, trois tailles,
aucun module Test ni sauvegarde dans le pack. Les 415 ressources historiques
de worldgen comparées à 227 sont identiques ; seul l’alias public est avancé.
- SHA-256 MRpack : `5e808121a2b1e3e358c58a2bb4616592521a4bd54cb18fe25c3ab68b05a4ee9d`.
- SHA-256 JAR : `4a62259e1d941079de44237ed3a342a5a5e7fbe443fce07826fda76be2dd2a58`.
Nouvelle visite `diagonal228-visit`, monde `Sanctuary-Diagonal-228-0`, copiée
depuis le nouveau monde de contrôle arrêté. Graine 0, île principale Petit,
quatre expansions de 1024 prêtes, créatif avec commandes, 32 chunks et simulation
5. Ouverture Vulkan confirmée le 5 octobre à 21:54:28 dans
`build/diagonal228-solo.log` : `DIAGONAL228_VISIT_OPEN`, position 848, 267, -496.
| Région | Relais X, Y, Z sur la graine 0 |
| --- | --- |
| NE jardin pâle/marais | 848, 219, -496 |
| SE mangroves/cavités | 560, 377, 816 |
| SO savane/désert | -832, 217, 512 |
| NO plaines/taïga | -608, 238, -768 |
Le rendu reste à apprécier par le joueur. Ces essais ne constituent pas une
mesure de fluidité ni une visite exhaustive des tailles Moyen/Grand. Aucun
essai Windows, aucune modification d’un ancien monde, aucune publication de
canal packwiz ou synchronisation Prism dans ce chantier.
+76
View File
@@ -0,0 +1,76 @@
# Sous-bois pâles et visite des neuf îles — beta.231
Retours R049–R050 du 5 octobre 2026. Base `42e42fe`, branche
`codex/pale-understory-beta231`. Correctif livré ; nouvelle visite ouverte.
Compléter les sols inférieurs du jardin pâle : herbes, fleurs natives
(eyeblossoms), tapis et mousse suspendue sous les feuillages. Conserver les
formes, arbres, dripstones, marais supérieurs et géologie de savane validés.
L’eau reste facultative, seulement dans les cuvettes naturelles existantes.
Nouvelle révision de génération, sans modifier les anciens profils ou mondes.
Préparer une nouvelle visite isolée sur la graine 0 : Sanctuary Island Grand
(1024 blocs), huit expansions de 1024, toutes activées. Le joueur a choisi ensuite une ouverture immédiate,
avec chargement des zones restantes pendant l’exploration. Créatif, commandes, Vulkan, distance 32 chunks. Aucun changement
à la génération normale du rythme d’ouverture des expansions. Pas de
publication de canal, ni modification de l’instance Prism personnelle.
Le correctif réutilise exactement le champ de terrain 230 et ses biomes.
Une décoration supplémentaire parcourt les sols inférieurs réels du NE,
à ciel ouvert ou abrités : herbe courte/haute, tapis de mousse pâle et fleurs
fermées natives, avec leur transition jour/nuit. Petits rideaux de mousse
sous le feuillage pâle. Les regroupements suivent des bruits lents ; aucune
plante ne remplace eau, roche, tronc, dripstone ou végétation existante.
Cinq GameTests passent, dont support réel des plantes, double hauteur,
protection des obstacles et conservation du champ accepté sur 0/42.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal231 -PsanctuaryAtlasOnly=true` passe
en 2 min 34 s. MRpack normal vérifié (Minecraft 26.3 / Loader 0.19.5), trois
tailles, JAR exact, sans Test ni monde ; 439 ressources historiques identiques.
SHA-256 pack : `3a9763cb81e088a0beeb4e4044dd35586a590c75f6c2f9a035937c8df4dee14c`.
SHA-256 JAR : `8ba7b19428094e01ac9f3b6b3f8054343bc7a3df60f786260f01b487ee2f675a`.
Copie `Downloads/Sanctuary-beta.231.mrpack`, reçu
`build/diagonal231-mrpack-receipt.json`.
La préparation complète est explicite :
`python3 scripts/worldgen_lab.py verify --run archipelago231-check0 --profile large
--seed 0 --checks diagonal231 --all-islands --memory 2048`.
Elle utilise les paiements natifs, conserve l’interruption/reprise du premier
relais, puis charge les neuf emprises avec une limite de quatre chunks en
cours. Chaque chunk doit atteindre FULL, tickets libérés au fur et à mesure.
Rapport final et arrêt propre requis avant de copier la nouvelle visite.
Sur la graine 0 en Grand, les quatre contrôles natifs des diagonales passent :
NE, dans un chunk inférieur sondé, 75 blocs d’herbe, 24 fleurs, 37 tapis,
8 mousses suspendues, 44 troncs pâles, 105 dripstones et 5 lucioles supérieures.
Ces nombres attestent leur présence ; ils ne mesurent pas toute l’île.
Savane : 7731 blocs jaunes et 101 de pierre dans les échantillons ; cavités
soufre/dripstone présentes. Mangroves et plaines conservent leurs habitats.
Les huit activations réelles aboutissent, relais présents, journal relu :
neuf îles de diamètre 1024 en état `ready`, génération 231, graine racine 0.
La prégénération intégrale visait 41 458 chunks ; le joueur choisit de lancer
le solo immédiatement. Arrêt demandé à 23:06:09, sauvegarde normale achevée,
dernier jalon 256/41 458 (en plus des zones d’arrivée et échantillons).
Ce contrôle interrompu volontairement n’est **pas** une prégénération complète
réussie ; les zones restantes se chargent pendant l’exploration.
Reçu : `build/worldgen-lab/archipelago231-check0/large/0/visit-readiness.json`.
Anciennes visites conservées, aucune validation Windows ou mesure de fluidité.
Visite `archipelago231-visit/large/0`, monde `Sanctuary-Diagonal-231-0`,
créée depuis le serveur arrêté et ouverte à 23:07:33 le 5 octobre 2026.
Vulkan confirmé, marqueur `DIAGONAL231_VISIT_OPEN`, départ (0, 320, 0),
créatif/vol/commandes, rendu 32 chunks, simulation 5. Les données du labo
restent dans `build/` ignoré. Aucun ancien monde modifié.
| Île | Centre X/Z | Commande de visite |
| --- | --- | --- |
| Nord | 32 / -1984 | `/sanctuary expansion visit r2yyuk27a85c8p` |
| Nord-Est | 736 / -960 | `/sanctuary expansion visit r7vapqz7anv7c` |
| Est | 1776 / -208 | `/sanctuary expansion visit r2zwcc9ktr14ds` |
| Sud-Est | 960 / 736 | `/sanctuary expansion visit r3bswui5wc00kk` |
| Sud | 0 / 1536 | `/sanctuary expansion visit r391kr4y3lwlzy` |
| Sud-Ouest | -960 / 736 | `/sanctuary expansion visit rclt83rncgsv1` |
| Ouest | -1792 / -192 | `/sanctuary expansion visit r3r7b6q184g2a1` |
| Nord-Ouest | -736 / -960 | `/sanctuary expansion visit r81ebztggj346` |
+97
View File
@@ -0,0 +1,97 @@
# Marais étagé et cavités sèches — beta.229
Retour R047 sur beta.228, 5 octobre 2026. Base `b77d71b`, branche
`codex/wetland-reset-beta229`. Nouveaux profils uniquement ; aucun ancien
monde, chunk ou identifiant de génération migré.
## Contrat
Refaire entièrement le NE : même géométrie que la savane amincie validée,
chênes pâles sur les sols abrités sous la roche, marais en haut. Lucioles
uniquement dans le marais supérieur. Eau facultative, seulement quand le
relief possède une dépression fermée ; abandon des lacs à niveau constant.
SO : soufre et dripstone dans les parties inférieures, sans lush caves et
sans changer la surface. SE mangrove entièrement validé ; NO conservé
provisoirement malgré les réserves esthétiques, aucune retouche des deux.
## Implémentation
Le NE réutilise le champ de densité SO 228, avec la graine propre à cette
expansion. Épaisseur variable et plateau restent ceux de ce champ ; seules
les matières et les habitats changent. Sous les 12 derniers blocs du toit,
biome jardin pâle ; en haut, biome marais. Plantation native des chênes pâles
sur des sols naturels de 2 × 2 avec le dégagement nécessaire sous un surplomb.
Les buissons à lucioles sont posés sur le sol supérieur réel, jamais sur des
feuilles ni dans le jardin pâle inférieur.
Aucune simulation hydrologique ni construction de cuvette : une recherche
locale bornée accepte seulement des dépressions d’un bloc de profondeur,
avec deux blocs de fond solide et des murs naturels continus. Si une paroi
manque, toute la mise en eau est abandonnée. Ces mares peuvent être rares
ou absentes suivant la graine. Les biomes NE n’ajoutent aucune source native.
SO : régions de soufre/cinabre ou dripstone sous le plateau, dans les
cavités du terrain existant. Formations natives de pics de soufre et de
speleothèmes ; aucun creusement nouveau ni décor lush. Les deux autres
champs et leurs décorations restent en révision 228.
## Vérifications
Huit GameTests ciblés passent (`build/diagonal229-tests.log`). Sur les graines
0/42, identité du champ NE avec le SO 228, habitats séparés en hauteur,
65/46 emplacements de chênes pâles sur les grilles sondées, mares toutes
bordées de matière naturelle, déterminisme, cavités SO et conservation des
champs SE/NO contrôlés. Les nombres d’emplacements ne représentent pas un
inventaire exhaustif des arbres de l’île.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,diagonal229 -PsanctuaryAtlasOnly=true` réussit
en 2 min 38 s. Contrôles natifs 0/42 réussis, avec quatre offres réelles,
refus des doubles offres, consommation correcte, interruption/reprise,
49 chunks FULL par activation et relais utilisables.
Dans les chunks sondés au NE : 40/44 blocs de tronc pâle et 9/4 buissons à
lucioles supérieurs ; racines sous des surplombs et aucune luciole basse.
Au SO : 32/37 pics de soufre et 9/76 pointed dripstone ; aucun décor lush
trouvé dans les cavités inspectées. Ces chiffres attestent la présence,
pas l’abondance globale. Les cavités lush/dripstone SE restent présentes.
Lecture des deux sauvegardes arrêtées (`build/diagonal229-water-audit.json`) :
52/51 chunks NE FULL, 36/12 colonnes de racines pâles, aucune cellule d’eau.
Les rares mares du plan théorique n’étaient donc pas présentes dans cet
échantillon natif ; leur fréquence visuelle reste à apprécier ailleurs.
Aucun ancien monde n’a été modifié.
## Artefact local
MRpack normal `build/Sanctuary-beta.229.mrpack`, copie dans Downloads.
Archive et JAR construits vérifiés, Minecraft 26.3 / Fabric Loader 0.19.5,
trois tailles publiques, aucun module Test ni sauvegarde. Les 421 ressources
historiques de worldgen comparées à 228 sont identiques ; seul l’alias public
est avancé. Reçu : `build/diagonal229-mrpack-receipt.json`.
- SHA-256 MRpack : `788b3aa5898f12aed4570224beb4dd44505eb9e5cae89cefdef9fe7549c85f8b`.
- SHA-256 JAR : `50fd7d6c04c9060abf3eca89bbaf936b39c6e9e82051d2d6559f197f425fdda9`.
Aucune validation Windows ni mesure de fluidité. Aucun ancien monde modifié,
aucune publication du canal packwiz ni synchronisation Prism.
## Visite
Nouvelle visite isolée `diagonal229-visit`, monde `Sanctuary-Diagonal-229-0`,
copiée depuis le nouveau contrôle graine 0 arrêté. Île principale Petit,
quatre expansions de 1024 prêtes, créatif avec commandes, vue 32 chunks,
simulation 5. Ouverture Vulkan confirmée à 22:23:49 le 5 octobre 2026 :
`DIAGONAL229_VISIT_OPEN` dans `build/diagonal229-solo.log`. Rendu à apprécier
par le joueur.
| Région | Relais X, Y, Z sur la graine 0 |
| --- | --- |
| NE marais / jardin pâle inférieur | 856, 225, -496 |
| SE mangroves / cavités | 560, 377, 816 |
| SO savane / cavités sèches | -832, 217, 512 |
| NO plaines / taïga | -608, 238, -768 |
Exemple de pied de chêne pâle observé dans la sauvegarde : 475, 174, -541.
Le joueur démarre au-dessus du relais NE. Anciennes visites conservées.
+84
View File
@@ -0,0 +1,84 @@
# Est — cratère strié et arbres enracinés, beta.224
## Contrat du 5 octobre 2026
Retour après visite beta.223 : relief général, arbres et ravins appréciés.
Arbres de jungle observés sur d’autres arbres ; cratère jugé trop camouflage.
Conserver la géométrie et retravailler ses matériaux : lignes concentriques
du fond vers le sommet, stries verticales/hachures qui s’estompent en hauteur,
plus d’obsidienne, magma à proximité, transitions légèrement bruitées.
Captures évoquées sans chemin fourni : aucune capture inspectée.
Branche `codex/east-crater-beta224`, base `9063e16`.
## Portée
Nouveaux profils 224 uniquement. Le champ de densité, les hauteurs, ravins,
cavités, lave et emplacements de monuments reprennent exactement 223 pour une
même graine d’expansion. Habillage minéral de la surface et des sept blocs sous-jacents du
cratère uniquement, sans nouvelle simulation ni passe de relief.
Les grandes jungles utilisent les mêmes arbres avec un filtre de sol avant
plantation, absent du placement custom 223. Tables natives de bambou, pandas
et autres arbres conservées. Île principale, Nord, Sud et Ouest conservés.
Anciennes sauvegardes et profils intacts ; essai dans un nouveau laboratoire.
## Vérifications ciblées
Java 25, Minecraft 26.3, Fabric Loader 0.19.5 et API 0.160.5+26.3.
Sept GameTests réussis (`base204,east223,east224`) :
- Conservation exacte des hauteurs, densités, cavités et volumes de lave entre
223 et 224 sur quatre graines d’expansion ; aucun déplacement des monuments.
- Matériaux hors du traitement du cratère inchangés ; obsidienne plus abondante
et magma présent sur la rive découverte, pas seulement sous la lave.
- Admission du grand arbre refusée sur feuilles, troncs, air et pierre ;
admise sur herbe, terre et podzol. Le tag natif `minecraft:dirt` de 26.3
ne contient ni herbe ni podzol : le filtre utilise les sols explicites et
la règle native de survie du jeune arbre, testés depuis le registre chargé.
- Profils de création et anciens profils chargés, anciens contrôles Est 223
et conservation de l’Ouest validé.
Deux serveurs natifs isolés à 2 Gio passent la demande par ancre, le refus du
mauvais paiement, l’interruption/reprise et la préparation de 62 chunks.
Même position et graine d’expansion qu’en 223 ; diamètres 1024 :
| Graine monde | Centre X/Z | Graine expansion | Tronc maximal | Pieds suspendus détectés |
| --- | --- | --- | --- | --- |
| 42 | 944 / 144 | 207124680538740498 | 41 blocs | 0 |
| 0 | 944 / -144 | 118376680751547919 | 44 blocs | 0 |
La recherche de pieds suspendus porte sur 24 chunks par graine (huit par
ambiance de jungle) : terre située plus de cinq blocs au-dessus du terrain
avec un tronc dessus. Ce contrôle et le filtre de placement corrigent le cas
identifié ; ils ne constituent pas une inspection exhaustive de tous les arbres.
Les mêmes relevés comptent respectivement 840/1 565 bûches, 2 883/4 646 feuilles
et 2 748/2 094 bambous. Deux coffres à butin et deux distributeurs dans chaque
temple, relais complet, 919/957 colonnes de lave et 39 464/38 928 contrôles
de parois sans fuite.
Reçus ignorés : `build/east224-gametest-passed.log` et
`build/worldgen-lab/east224-checked{42,0}/small/{42,0}/{cold,warm}.json`.
## Livraison
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,east223,east224 -PsanctuaryAtlasOnly=true`
réussit en 5 min 25 s. Log : `build/east224-build.log`.
MRpack normal vérifié et copié dans `~/Downloads` : archive intègre, JAR exact,
trois tailles, filtre de plantation et 371 ressources de génération historiques
strictement identiques à beta.223 (hors alias public du preset). Aucun monde ni
module de test embarqué. Reçu : `build/east224-mrpack-receipt.json`.
SHA-256 de `Sanctuary-beta.224.mrpack` :
```text
01aed4e6a0d9e2965ed8201372eef3aaecab3b0c6dce3fb84586895ed9076885
```
La visite neuve `Sanctuary-East-224-42` est ouverte le 5 octobre à 17:26:25,
Vulkan natif, rendu 32 chunks, simulation 5, créatif/vol/commandes. Position
initiale 547 / 351 / 144, face au volcan. Copie du serveur de contrôle arrêté,
identité de sources vérifiée ; anciennes visites conservées.
Le rendu des stries reste à apprécier par le joueur. Pas de nouvelle campagne graphique Windows,
ni d’exploration complète Medium/Large. Aucune modification d’ancien monde,
publication de canal ou mise à jour Prism.
+85
View File
@@ -0,0 +1,85 @@
# Est — volcan flottant et jungles, beta.223
## Contrat du 5 octobre 2026
Après la livraison des déclinaisons de roche jaune (beta.222), le joueur valide
l’Ouest 221 et demande le laboratoire Est. Volcan actif, lave dans le cratère,
jungle sur les flancs ; la silhouette historique est explicitement rejetée.
Il faut un vrai cône volcanique flottant, un temple de jungle, du bambou,
des pandas et des jungles clairsemées ou denses avec de très grands arbres.
Diamètre de premier essai : 1024 blocs, dans la direction de l’ancre Est.
Branche `codex/east-volcano-beta223`, base `2d656d9`.
## Portée
Nouveaux profils 223 Small/Medium/Large uniquement ; aucun profil 222 de
terrain n’existe (222 ajoutait uniquement des blocs). Île principale, Nord,
Sud et Ouest conservent leurs générateurs validés. Les mondes et journaux
historiques sont conservés. Nouveau laboratoire et nouvelle activation ;
aucune expansion ajoutée à une sauvegarde personnelle.
Silhouette conique asymétrique, bouche décentrée et légèrement elliptique,
ravines ramifiées par bruit cohérent, épaulement ancien et affaissement local
du bord du cratère. Répartition des jungles par nappes de bruit, sans secteurs
radiaux répétés. Couronne sommitale irrégulière et cuvette de lave
fermée, dessous suspendu irrégulier. Roche volcanique au sommet, sol végétal
sur les flancs. Biomes de jungle et bambou avec végétation native, dont une
variante de grands arbres. Temple natif avec pièges et coffres, fondations
locales. Relais sur le flanc extérieur, éloigné du cratère.
Pas d’éruption animée ni de simulation géologique/hydrologique globale.
## Vérifications
Le 5 octobre 2026, Java 25, Minecraft 26.3 et Fabric Loader 0.19.5 :
- `./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,east223,rock222 -PsanctuaryAtlasOnly=true`
réussit en 3 min 34 s. Sept GameTests ciblés passent également lors du contrôle
préalable : profils historiques et actuels, roche jaune, forme et lave sur
quatre graines, direction Est, égalité du relief et des décors Ouest 221/223.
- Deux serveurs natifs isolés, graines 42 et 0, mémoire 2 Gio : demande par ancre,
refus du mauvais paiement, interruption à 0/62 chunks et reprise persistée.
Les deux expansions deviennent disponibles avec 62 chunks préparés ; la suite
est générée à l’exploration.
- La cuvette est contrôlée dans les chunks réels : 919/957 colonnes de lave et
39 464/38 928 vérifications de parois pour 42/0. Aucun débouché vers le vide
détecté dans ces contrôles.
- Chaque temple contient deux coffres à table de butin et deux distributeurs
piégés ; 1 157 blocs de maçonnerie relevés. Le relais possède ses 2 069 blocs
attendus et une offre utilisable.
- Sur 24 chunks par graine, huit de chaque ambiance de jungle : 1 501/1 446
bûches, 3 747/4 398 feuilles, 2 689/2 294 bambous ; tronc continu maximal de
44 blocs sur les deux graines. Les tables natives du biome bambou comprennent
les pandas ; leur nombre réel n’est pas une assertion de cette campagne.
- MRpack vérifié : ZIP, JAR exact, versions, trois tailles, profils historiques,
biomes Est, tables de pandas, grands arbres, temple et déclinaisons de roche
jaune ; aucun monde ni module de laboratoire embarqué.
Les logs et reçus ignorés sont `build/east223-build-final.log`,
`build/east223-natural-gametest.log`, `build/east223-mrpack-receipt.json` et
`build/worldgen-lab/east223-checked{42,0}/small/{42,0}/{cold,warm}.json`.
| Graine du monde | Centre de l’expansion X/Z | Graine de l’expansion | Temple X/Y/Z |
| --- | --- | --- | --- |
| 42 | 944 / 144 | 207124680538740498 | 835 / 271 / 418 |
| 0 | 944 / -144 | 118376680751547919 | 961 / 300 / 150 |
## Livraison et visite
`Sanctuary-beta.223.mrpack`, copié dans `~/Downloads`, SHA-256 :
```text
ac31ff8203dca95265c7d0af545bab231697ad295b48eade2ee115090a9597e6
```
La visite neuve `Sanctuary-East-223-42` est ouverte le 5 octobre à 17:09:45,
Vulkan natif, rendu 32 chunks, simulation 5, créatif/vol/commandes. Position
initiale 547 / 351 / 144, face au volcan. Copie du laboratoire arrêté,
identité de sources vérifiée ; les anciennes visites sont conservées.
La roche jaune, ses escaliers et dalles de beta.222 sont inclus.
Le rendu naturel reste à apprécier par le joueur : les contrôles numériques
ne constituent pas une validation esthétique. Essais natifs Small sur deux
graines ; Medium/Large sont assemblés et leurs profils vérifiés, sans nouvelle
campagne d’exploration complète. Aucun essai Windows ni simulation d’éruption.
Pas de publication du canal packwiz ni de mise à jour de l’instance Prism.
+53
View File
@@ -0,0 +1,53 @@
# Les quatre îles dans Sanctuary normal
## Validation du 5 octobre 2026
Le joueur valide le rendu beta.224 et demande que les quatre îles soient
disponibles dans Sanctuary de base, sans Sanctuary Test.
Branche de vérification : `codex/four-islands-base224`, base `4554ea1`.
L’audit confirme que cette intégration est déjà effective dans le mod et le
MRpack normal beta.224. Aucun transfert supplémentaire de code n’est nécessaire.
Cette livraison documentaire ne change ni les binaires ni leur version.
| Ancre | Expansion validée | Contenu principal |
| --- | --- | --- |
| Nord | Glaciale | Taïgas géantes, glace, améthyste, igloos et manoir |
| Sud | Aride | Canyons colorés, forêts, sentiers et pyramide piégée |
| Ouest | Océanique | Océan, monument, épaves et berges en roche jaune |
| Est | Volcanique | Volcan actif, jungles, bambou et temple de jungle |
Créer un **nouveau monde Sanctuary**, avec le choix Petit/Moyen/Grand.
Les expansions de 1024 blocs sont débloquées par leurs ancres cardinales
et leurs demandes de matériaux ; elles ne sont pas toutes prégénérées au départ.
Les anciens mondes conservent leur génération enregistrée.
## Contrôle de l’intégration
- Le preset public `sanctuary:sanctuary` utilise `island224_medium`. Le sélecteur
du mod normal propose également les profils Small et Large de cette génération.
- `SanctuaryMod` enregistre directement les générateurs, l’adaptateur d’expansion
et les interactions des ancres. Aucun de ces chemins ne dépend de l’activation
de `QuickTestMod`. Les anciens noms Java et identifiants `sanctuary_test:*`
sont conservés pour la compatibilité ; ils ne constituent pas une dépendance
au module optionnel.
- Vérification du MRpack normal : générateurs, structures et biomes des quatre
directions présents pour les trois tailles ; un seul JAR Sanctuary, aucune
dépendance au module `sanctuary_test`, aucun monde ni JAR Sanctuary Test inclus.
- La création conserve les types vanilla et une seule entrée publique Sanctuary.
- Le build complet et les sept GameTests beta.224 ont déjà réussi, dont le
chargement des profils sans le module de labo. Ils ne sont pas relancés pour
cette modification documentaire. Les essais natifs des différentes îles sont
décrits dans leurs tickets ; cet audit ne revendique pas une nouvelle partie
complète avec les quatre îles simultanément.
Reçu local : `build/four-islands-base224-receipt.json`.
Artefact standard : `build/Sanctuary-beta.224.mrpack`, copie identique dans
`~/Downloads/Sanctuary-beta.224.mrpack`, SHA-256 :
```text
01aed4e6a0d9e2965ed8201372eef3aaecab3b0c6dce3fb84586895ed9076885
```
Cette validation ne migre aucun monde et ne modifie pas le canal packwiz ou
l’installation Prism personnelle.
+103
View File
@@ -0,0 +1,103 @@
# WG-206 — dessous aminci et cavités froides
Branche `codex/north-cavities-beta206`, depuis beta.205 (`bf5df38`).
## Contrat
Le créateur valide une épaisseur variable sous le paysage du Nord, des cavités
froides et humides, et des failles ouvertes d’améthyste bordées de calcite et de
basalte. La surface et les grandes taïgas de beta.205 sont la référence.
La redistribution diorite / granit / andésite reste une proposition distincte.
Nouveau profil 206 uniquement, expansion Nord de 1024 blocs. Île de départ,
autres directions et profils historiques conservés. Aucun monde existant
converti, aucun chunk régénéré. Contrat des essais : nouveaux mondes de
développement, graines 42 et 0 ; activation et reprise du Nord explicitement
réservées à ces nouvelles sauvegardes de test. Aucun canal public ni instance
Prism à modifier.
## Implémentation
- Masque soustractif sous le bruit natif 205 : épaisseur cible continue de
40–99 blocs, contrôlée par deux bruits, dessous irrégulier. Les vingt blocs
supérieurs du champ natif restent protégés. Le masque ne rajoute aucune
montagne ni terrasse ; la roche déjà absente reste absente.
- Failles sinueuses dans la matière, avec améthyste, calcite et basalte lisse
organisés autour du même champ de fissure. Cristaux orientés vers l’air et
quelques blocs d’améthyste bourgeonnante.
- Dépôts de glace compacte et de glace bleue dans les cavités, argile localisée
et lichens. Biomes nordiques 206 distincts, sans les placements de champignons
ni les géodes fermées des profils précédents ; minerais conservés.
- Eau peu profonde dans le sol existant des cavités : un bloc de roche ou de
glace peut être remplacé par l’eau dans une poche de 6–96 blocs, sans ellipse
ni terrasse ajoutée. Contours en bruit mondial, fond et berges contrôlés sur
les blocs réels ; tout débouché est refusé. Recherche locale bornée, sans
grande hydrologie ni travail périodique en partie.
## Vérifications
- `./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north205,north206 -PsanctuaryAtlasOnly=true` :
succès (journal `build/north206-check-build-water.log`). Six GameTests ciblés
et contrôles purs passés. La suite native historique complète n’est pas revendiquée.
- Deux derniers garde-fous locaux des poches d’eau ont ensuite été recompilés :
lectures limitées aux chunks voisins et refus d’approfondir une poche déjà
remplie. Reconstruction finale avec `:sanctuary:runGameTest build assemblePack
assembleTestPack :sanctuary-test:exportDuoLaunch -x :sanctuary:check`, mêmes
propriétés de sélection : six tests repassés, succès en 22 s. Les contrôles
purs déjà passés n’ont pas été répétés pour ces deux garde-fous ; les essais
natifs ci-dessous utilisent ce JAR final.
- Profils Small, graines **42 et 0**, nouveaux mondes `north206-final` et
`north206-final0` : offrande réelle, rejet d’offrande incorrecte et de doublon,
interruption à 0/58, reprise des 58 chunks FULL et message SGA vérifiés.
Cinq igloos et une cave de guérison complets dans chaque monde.
- Comparaison au champ 205 de la même graine, sur une grille de relevé :
| Graine | Échantillons de roche 205 → 206 | Épaisseur maximale relevée 205 → 206 | Hauteurs de surface différentes | Plus grand tronc vérifié |
| --- | --- | --- | --- | --- |
| 42 | 17 968 → 9 279 (−48,4 %) | 179 → 91 blocs | 0 | 59 blocs |
| 0 | 17 084 → 8 887 (−48,0 %) | 177 → 90 blocs | 0 | 61 blocs |
- Dans les régions de faille examinées : 384 cristaux sur la graine 42 et 334
sur la graine 0 ; respectivement 2 429 et 2 425 blocs d’améthyste avec au moins
une face ouverte sur l’air. Calcite et basalte présents. Ce ne sont pas des
totaux à l’échelle de l’île.
- Eau vérifiée dans les blocs réels, avec fond et berges fermés : 11 positions
sur la graine 42 et 61 sur la graine 0. Cavités contenant air et glace, glaciers
de surface et taïgas géantes contrôlés. Ce contrôle des fondations ne remplace
pas un essai prolongé après modification des berges par le joueur.
- 3 456 cellules de biome stockées comparées au champ mondial par graine :
**aucune incohérence** aux jointures examinées. Densité de l’île initiale
identique octet par octet à la 205 pour Small, Medium et Large ; classes et
ressources historiques 205 conservées.
L’essai préliminaire a révélé un raccordement manquant du décor au générateur
hôte, corrigé avant ces essais finaux. La première recherche de petits bassins
strictement préexistants ne produisait pas d’eau vérifiable ; elle a été remplacée
par les poches peu profondes décrites ci-dessus. Une tentative démarrée avant la
fin de l’assemblage utilisait encore l’ancien JAR et n’est pas une preuve de la
version finale.
Les chiffres sont des échantillons, pas un inventaire exhaustif. Les essais
natifs supplémentaires génèrent plusieurs régions de visite et ne constituent
pas un benchmark comparatif de vitesse. Aucun essai Windows ni verdict esthétique
sur l’ensemble du paysage n’est revendiqué.
## Livraison locale
MRpack normal `build/Sanctuary-beta.206.mrpack`, copié dans `~/Downloads/` : ZIP
intact, JAR identique au build, trois tailles courantes et historiques présentes,
aucun mod Test ni sauvegarde embarqués. Minecraft 26.3 / Fabric Loader 0.19.5.
SHA-256 : `a0d2df3d207be0844af6cd4a3a578fd68c860dd0d3a28048f3b58725bb599fc7`.
Reçu : `build/north206-mrpack-receipt.json`.
Nouveau solo préparé depuis le monde 42 vérifié, dans `north206-visit`, avec
commandes, vue 32 chunks, simulation 5 et Vulkan. Arrivée près d’une paroi
d’améthyste ouverte en **(-463.5, 205, -1151.5)**. Autres repères de cette graine :
petite eau vers **(-311, 187, -732)** ; cavité glacée sous **(-416, 249, -728)** ;
grandes taïgas vers **(-176, 249, -720)**. Les altitudes de repère des taïgas et de
la cavité glacée sont celles de la surface, pas des téléportations souterraines.
Le client confirme le backend Vulkan et `NORTH206_VISIT_OPEN` à l’emplacement
prévu ; le rendu global reste à apprécier pendant la visite. Aucune publication
du canal, aucun déploiement Prism ni modification du monde 205.
+129
View File
@@ -0,0 +1,129 @@
# WG-207 — neige, couronnes et manoir du Nord
Branche `codex/north-landscape-beta207`, depuis beta.206 (`6f78054`).
## Contrat
Accord du créateur sur les trois suites du retour R004 : taïga partiellement
enneigée, reliefs Voronoi localisés sur les hauts plateaux et manoir procédural
inspiré du manoir vanilla. Le bois du manoir doit être de l’épicéa (spruce).
Conserver le diamètre de 1024 blocs, le dessous aminci et les grandes failles
d’améthyste validés en visite. Ne pas transformer toute la forêt en neige.
Profil neuf 207 exclusivement. Les profils 206 et antérieurs gardent leur
génération. Aucun monde existant modifié ni chunk régénéré. Contrat des essais :
nouveaux mondes de développement, graines 42 et 0, avec activation et reprise
du Nord pour contrôler la génération et préparer une visite. Aucun déploiement
Prism, publication du canal ou modification de sauvegarde personnelle.
## Implémentation
- Jusqu’à cinq couronnes sur des épaules hautes et soutenues, avec distances
de Voronoi, déformation du contour par bruit et cols entre les sommets.
Ajout local de quelques dizaines de blocs, plafond à Y=308 ; aucune terrasse
globale. Le champ de densité sous le toit initial reste celui de la 206.
- Nappes de neige de plusieurs échelles sur la taïga, avec deux variantes de
biomes froids et les variantes tempérées conservées. Placement sur le sol
réel sous la canopée, remplacement des petites plantes, état enneigé de
l’herbe/podzol. Les troncs, l’eau et les plantes non admissibles sont protégés.
La passe lit les cellules de biome déjà stockées au lieu de refaire le calcul
complet des cavités pour chaque colonne de neige.
- Assemblage natif des pièces du manoir, avec disposition dépendante de la
graine, pièces meublées, coffres et occupants vanilla. Conversion locale des
bois principaux en épicéa, panneaux en calcite, soubassements en tuf taillé et
tapis froids. Les propriétés des escaliers, dalles et portes sont conservées.
- Recherche bornée d’un site adapté à l’emprise réelle, hors igloos, arrivée,
relais et couronnes ; contrôle des appuis colonne par colonne. Aucun
terrassement massif ni fondation sans limite vers le vide. Si une graine ne
possède pas de site admissible, le manoir est omis avec un diagnostic plutôt
que de faire échouer l’expansion.
- Approche de trois blocs de large en escaliers de tuf, raccordée au terrain
devant l’entrée ; appuis et dégagement contrôlés dans les essais natifs.
## Vérifications de compilation
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north206,north207 -PsanctuaryAtlasOnly=true` :
succès, contrôles purs et six GameTests ciblés passés. Journal :
`build/north207-check-build.log` (5 min 42 s).
Après les dernières corrections locales (accès, portillon en épicéa et lecture
des biomes pour la neige), reconstruction et six GameTests repassés avec
`:sanctuary:runGameTest build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch -x :sanctuary:check`, mêmes propriétés de sélection.
Journal final : `build/north207-final2-build.log`, succès en 55 s. Les contrôles
purs historiques déjà passés n’ont pas été répétés à cette étape. La suite
historique complète des GameTests n’est pas revendiquée.
Les tests ciblés vérifient le maintien des anciens presets dans les trois tailles,
la conservation du dessous 206, l’étendue limitée du nouveau relief et les
propriétés d’orientation des escaliers/portes après conversion en épicéa.
Les trois fichiers de densité de l’île initiale sont identiques à ceux de la 206.
## Essais natifs sur le build final
Profils Small, nouveaux mondes `north207-final` (42) et `north207-final0` (0).
Offrande réelle, refus des offrandes incorrectes et des doublons, interruption
à 0/58 puis reprise des chunks préparés et annonce SGA : succès. Cinq igloos
et une cave de guérison complets sur chaque graine.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Couronnes / ajout maximal relevé | 5 / 52 blocs | 5 / 45 blocs |
| Colonnes relevées avec élévation > 2 blocs / autres | 191 / 1 767 | 192 / 1 790 |
| Échantillons profonds identiques à la 206 | 52 989 | 51 958 |
| Améthyste / faces ouvertes dans la région inspectée | 7 731 / 1 928 | 12 494 / 3 567 |
| Sols enneigés dans la zone de taïga enneigée inspectée | 2 225 | 1 514 |
| Sols dégagés / enneigés dans la taïga tempérée inspectée | 2 209 / 0 | 2 372 / 0 |
| Pièces natives du manoir | 544 | 547 |
| Fondation maximale du manoir | 5 blocs | 6 blocs |
| Coffres conservant une table de butin native | 5 | 5 |
| Cellules de biome stockées contrôlées / incohérences | 1 944 / 0 | 1 944 / 0 |
Les deux manoirs ont des planchers et escaliers en épicéa, des panneaux en
calcite et du tuf taillé. Aucun bloc de planches de chêne/chêne noir ni feuille
d’épicéa envahissante dans les volumes examinés. L’approche possède trois
marches centrales contrôlées avec support et deux blocs d’air de dégagement.
Les autres coffres des pièces vanilla peuvent être des rangements sans table
de butin : ne pas confondre nombre total de coffres et récompenses.
Ces nombres sont des relevés locaux ou sur grille, pas un inventaire de l’île.
L’essai préliminaire `north207-a` avait déjà passé les contrôles de paysage et
de manoir ; les deux essais ci-dessus utilisent le JAR final, après optimisation
de la passe de neige et vérification supplémentaire de l’accès et du butin.
Les essais finaux tournaient en parallèle : leurs durées ne constituent pas
une comparaison de performance avec la 206.
## Livraison locale
MRpack normal `build/Sanctuary-beta.207.mrpack`, copié dans `~/Downloads/`.
ZIP intact, JAR identique au build final, profils courants et historiques des
trois tailles présents, aucun mod Test ni monde embarqué. Minecraft 26.3,
Fabric Loader 0.19.5. Reçu : `build/north207-mrpack-receipt.json`.
SHA-256 : `1f6caca543dddea4e961bba6737a9d4a0e2fbb2935fb17a3c2c94d8f1a7a635d`.
Nouvelle visite `north207-visit`, graine 42, copiée depuis le monde vérifié et
arrêté. Vue 32 chunks, simulation 5, commandes et vol créatif. Position initiale
préparée : **(49.5, 302, -1285.5)**, face au manoir. Repères de visite :
- Manoir : **(-27, 252, -1244)**.
- Taïga enneigée : **(-192, 251, -704)**.
- Taïga dégagée : **(32, 253, -720)**.
- Sommet ajouté : **(-448, 303, -1008)**.
Les repères indiquent les régions contrôlées ; certains sont au-dessus du sol
pour la visite en vol. Aucun canal public, serveur personnel ou instance Prism
modifié. Les mondes précédents sont conservés.
Le client confirme Vulkan / MoltenVK et `NORTH207_VISIT_OPEN` à **(49, 302,
-1286)** le 5 octobre à 02:28. La visite est ouverte ; le rendu reste à juger
par le créateur, sans prétendre à une inspection graphique exhaustive.
## Limites
Le manoir utilise les pièces et le butin natifs avec une palette propre au Nord.
Il ne possède pas de nouvelle entrée `/locate` ; son placement appartient au
générateur local. L’admission peut omettre le manoir sur une graine sans appui
admissible. Aucun jugement esthétique global, essai Windows ou benchmark
comparatif n’est déduit des vérifications techniques.
+77
View File
@@ -0,0 +1,77 @@
# WG-205 — Nord en bruit 3D natif
Branche `codex/north-noise-beta205`, depuis beta.204 (`2463db7`).
Retour : montagnes reliées et trop étirées, démarcations de biomes suivant les
chunks. Retirer la surélévation Peaks et les masses ajoutées au-dessus du bruit
de base ; habiller ensuite les surfaces réellement générées.
Contrat : nouveau profil 205 uniquement, Nord de 1024 blocs. Taïga géante,
glaces et cinq igloos conservés. Île de départ et profils 200–204 inchangés.
Aucune migration, aucun ancien chunk modifié. Essais sur nouveaux mondes de
développement, graines 42 et 0. Publication et installation Prism non demandées.
## Changements
Le Nord 204 cumulait la déformation verticale Peaks, un agrandissement horizontal
×2 / vertical √2 et les corps supérieurs de la densité Population. Le profil 205
utilise le champ natif sans ces déformations : enveloppe de 1024 directement,
bruit à son échelle d'origine et cavités conservées. Le champ n'est pas rehaussé
après coup pour former une chaîne de montagnes. Les anciennes classes 204 restent
inchangées et les autres directions conservent leur relief.
Le champ d'écologie 204 arrondissait X/Z par groupes de huit blocs. La hauteur,
la pente et les seuils de glace pouvaient alors changer d'un carré au suivant.
Le nouveau lecteur interpole les hauteurs entre les mesures ; il mesure à nouveau
une colonne quand un voisin est vide, sans fabriquer un sol par interpolation.
Les sols sont appliqués sur la vraie hauteur du chunk déjà généré. La ligne de
froid est ajustée au nouveau relief moins élevé, avec taïga géante, neige, pics
natifs et cavernes glacées ; les igloos cherchent des épaules soutenues.
## Vérifications — 5 octobre 2026
- Cinq GameTests ciblés passés (`base204,north205`, plus contrôle commun) :
presets courants/historiques sans Test, pente continue sur les frontières X/Z
négatives et positives, indépendance des workers, absence d'étirement et de
surélévation 205, conservation de l'ancien comportement 204 et des autres axes.
- Small, graine 42, monde neuf `north205-final` : offrande réelle, refus des doublons,
interruption/reprise, préparation de 58 chunks, cinq igloos et une cave de
guérison vérifiés. Centre `(-128, -944)`, arrivée `(-160, 253, -1024)`.
- Même graine : échantillons de surface Y=87–251, épaisseur maximale 179 blocs,
taïga géante avec un tronc de 59 blocs, cavité avec air et glace, glacier présent.
Zéro incohérence entre les 3 456 cellules de biome sauvegardées aux jointures
des zones contrôlées et le champ mondial. Reçus dans
`build/worldgen-lab/north205-final/small/42/`.
- Le premier diagnostic de grotte pointait le massif le plus épais, contenant
surtout de la roche. Le contrôle cherche maintenant une poche vide sous une
couverture suffisante avant de vérifier air et glace dans les chunks réels.
Le terrain n'a pas été modifié pour satisfaire ce diagnostic.
- Small, graine 0, monde neuf `north205-final0` : mêmes contrôles de l'expansion,
cinq igloos, cavernes et glaciers passés. Échantillons Y=84–255, tronc maximal
61 blocs. Zéro incohérence sur 3 456 cellules de biome supplémentaires. Le
contrôle de glacier inclut la glace normale, adaptée à ces altitudes, et
exige plus de 50 blocs glacés dans la zone examinée ; il ne suppose plus la
présence obligatoire de glace compacte ou bleue. La graine 0 en contient 117.
- `./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=base204,north205 -PsanctuaryAtlasOnly=true` : succès,
journal `build/north205-check-build.log`. La suite native complète n'est pas
revendiquée. Après la dernière correction du diagnostic Test, sa compilation,
ses tests et son build ont également passé ; une répétition inutile des tâches
du mod principal a été interrompue. Les deux essais natifs finaux utilisent
ce diagnostic corrigé. Le JAR du mod normal n'a pas changé après sa validation.
- MRpack normal exporté et vérifié : contenu ZIP intact, JAR identique au build,
preset public 205, profils historiques présents, aucun Test ou monde embarqué.
SHA-256 : `63023e8ff355afb94223bd697a71ae1d6ad72c80dca2f5c502664627f3d8d572`.
Les mesures de hauteur sont des échantillons, pas un relevé exhaustif de tous les blocs. La
continuité numérique et les biomes stockés sont testés ; l'appréciation visuelle
des transitions reste à confirmer en visite. Aucune validation Windows revendiquée.
## Livraison locale
`build/Sanctuary-beta.205.mrpack`, copié dans `~/Downloads/`. Une copie neuve du
monde vérifié est ouverte dans `north205-play`, graine 42, en créatif avec
commandes autorisées, Vulkan, vue 32 chunks et simulation 5. La visite commence
au-dessus de la taïga en `(-143.5, 331, -677.5)` ; `NORTH205_VISIT_OPEN` et
le backend Vulkan sont confirmés dans le journal. Aucun monde 204 n'est converti.
Le canal public et l'instance Prism ne sont pas modifiés.
+113
View File
@@ -0,0 +1,113 @@
# WG-208 — crêtes et replats glacés du Nord
Branche `codex/north-ridges-beta208`, depuis beta.207 (`bb10c24`).
## Contrat
Dernier affinage demandé avant clôture du Nord : remplacer les couronnes trop
rondes par des arêtes anguleuses, des sommets pointus et des entailles d'érosion.
Ajouter quelques replats à différentes altitudes avec des poches de glace.
Les formations de glace au-dessus restent une piste : privilégier quelques
aiguilles naturelles localisées, sans ajouter de bâtiment.
Conserver la taille de 1024 blocs, la taïga partiellement enneigée, le dessous
aminci, les failles d'améthyste, les igloos et le manoir en épicéa validés.
Profil neuf 208 uniquement. Les générations 207 et antérieures restent
disponibles et inchangées. Essais autorisés sur de nouveaux mondes de
développement, graines 42 et 0, avec activation/reprise du Nord. Préparer une
nouvelle visite solo Vulkan à 32 chunks ; aucune sauvegarde existante modifiée.
Aucun déploiement Prism ni publication du canal public.
## Réalisation
- Les mêmes cinq sites de relief qu'en 207 reçoivent des faces asymétriques.
La distance aux sites Voronoi sépare les massifs ; cinq plans inégaux dessinent
leurs arêtes. La pente reste marquée près du sommet au lieu de s'y arrondir.
Des entailles par bruit cohérent interrompent localement les flancs. Il ne
s'agit pas d'une simulation d'érosion ou d'une hydrologie globale.
- Jusqu'à deux replats par massif, de rayons 15 et 19 blocs, à des hauteurs
différentes. Ils sont admis uniquement au-dessus du toit rocheux existant,
avec raccordement progressif sur leur bord. Aucune quantification générale
des altitudes ni succession de terrasses sur toute l'île.
- Petites cuvettes de glace sur glace compacte, encaissées d'un bloc, et
aiguilles de glace sur certaines rives. Ces poches sont gelées jusqu'au fond :
elles n'ajoutent pas d'écoulements ni de nouvelles cascades.
- Densité profonde de la 206 conservée ; écologie et neige de la 207 réutilisées,
y compris leurs identifiants de biomes. Manoir et igloos adaptés au nouveau
champ de relief, avec leurs palettes et contenus conservés. Les anciens
générateurs et ressources 207 restent disponibles.
- Les calculs de sommet sont mémorisés par colonne dans un cache borné par
worker. Aucun parcours global supplémentaire à chaque tick.
## Vérification
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north207,north208 -PsanctuaryAtlasOnly=true` :
succès en 4 min 47 s, contrôles purs et sept GameTests ciblés passés.
Journal : `build/north208-check-build.log`. Les tests vérifient notamment les
anciens presets des trois tailles, le maintien de la densité profonde, le dénivelé
près des sommets, les appuis des cuvettes et le déterminisme entre workers.
La suite historique complète des GameTests n'est pas revendiquée.
Les dernières additions au diagnostic et au lanceur du module Test sont
recompilées et assemblées séparément, sans répéter les contrôles purs déjà passés :
`build/north208-lab-final.log`, succès. Les trois fichiers de densité de l'île
principale sont strictement identiques à la 207.
Le premier essai 42 passe tous les contrôles. Sur la graine 0, un replat
pouvait remplir un creux trop profond (élévation locale relevée de 94 blocs).
L'admission exige désormais un toit proche sur toute l'emprise ; une garde
par colonne empêche de combler les trous profonds. Un test de régression
reproduit une cavité sous un replat. Reconstruction finale avec `:sanctuary:runGameTest build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch -x :sanctuary:check`, sélection `base204,north208` :
succès en 1 min 1 s ; six tests ciblés, dont la régression du trou profond,
passés. Les contrôles purs précédemment passés ne sont pas répétés à cette étape.
Journal : `build/north208-final-build.log`. Les essais finaux `north208-final` (42) et `north208-final0` (0) passent sur le
JAR final. Offrandes réelles, refus des doublons, interruption à 0/58, reprise
jusqu'à 58 chunks FULL et annonce SGA contrôlés. Serveurs arrêtés proprement.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Sommets / élévation maximale relevée | 5 / 50 blocs | 5 / 44 blocs |
| Colonnes relevées élevées / autres | 183 / 1 775 | 197 / 1 785 |
| Dénivelé moyen à 8 blocs de chaque sommet | 6,6–9,1 blocs | 5,6–6,9 blocs |
| Replats glacés admis | 6 | 5 |
| Glace dans les cuvettes inspectées | 1 972 blocs | 2 000 blocs |
| Glace compacte des formations de rive inspectées | 360 blocs | 236 blocs |
| Échantillons profonds identiques à la 206 | 52 989 | 51 958 |
| Améthyste / faces ouvertes inspectées | 7 731 / 1 928 | 12 494 / 3 567 |
| Cellules de biome vérifiées / incohérences | 1 944 / 0 | 1 944 / 0 |
La neige et les forêts dégagées restent présentes. Cinq igloos et une cave de
soins par île. Manoirs de 544 et 547 pièces, aux mêmes emplacements qu'en 207,
avec trois marches d'accès vérifiées et cinq coffres à table de butin native
chacun. Les comptages sont des relevés locaux ou sur grille, pas un inventaire
exhaustif. Les essais simultanés ne constituent pas un benchmark comparatif.
## Distribution locale
MRpack normal exporté et contrôlé : archive ZIP intacte, JAR strictement identique
au build, profils 207 et 208 Small/Medium/Large présents, aucun module Test ni
monde embarqué. Minecraft 26.3 / Fabric Loader 0.19.5.
Reçu : `build/north208-mrpack-receipt.json`. Copie dans `~/Downloads/`.
SHA-256 : `bf1006efca537996fe4e33750ff08cbf821d2735bc03e5ce517da7fd62922845`.
## Limites
La retouche ne résout pas à elle seule le temps de chargement signalé. Aucun
benchmark comparatif, jugement esthétique global ni essai Windows n'est déduit
des vérifications. La clôture visuelle du Nord revient à la visite du créateur.
## Visite solo
Nouveau monde `Sanctuary-North-208-42` dans
`build/worldgen-lab/north208-final-visit/small/42/client/saves/`, copié depuis
l'essai final arrêté. Vue 32 chunks, simulation 5, créatif en vol et commandes.
Position initiale : **(-389.5, 325, -1060.5)**, face au sommet **(-448, 301, -1008)**.
Replat voisin : **(-457, 278, -1034)** ; second niveau : **(-406, 262, -986)**.
Manoir conservé : **(-27, 252, -1244)**. Anciennes visites préservées.
Le client confirme Vulkan / MoltenVK et `NORTH208_VISIT_OPEN` le 5 octobre
à 03:23:59, en **(-390, 325, -1061)**. Visite ouverte ; aucune inspection graphique
exhaustive ni validation esthétique du créateur n'est déduite de ce lancement.
+86
View File
@@ -0,0 +1,86 @@
# PAUSE-233 — navigation centrée et rédaction communautaire
Branche `codex/pause-community-beta233`, base beta.232, Minecraft 26.3.
## Résultat et compatibilité
Avec ou sans Atlas, les boutons gardent les mêmes groupes et le même ordre :
- Communauté : Habitant, Faction et Battle en haut de la colonne.
- Aventure : Discovery et Progression centrés verticalement dans l'écran.
- Partie : Options, World Options et Save and Quit en bas.
Sans carte, cette colonne est centrée horizontalement entre Gazette à gauche
et Notice Board à droite. Avec carte, elle reste à gauche, carte au centre et
les deux fils communautaires à droite. L'ordre clavier suit l'ordre visuel.
Les boutons deviennent plus compacts sur les petites hauteurs GUI pour conserver
les titres et l'espacement entre groupes. La petite barre sous les boutons
Communauté est supprimée dans les deux dispositions. En-tête et reprise en pied
conservés.
Le formulaire Notice Board ne propose plus Information/Project/Request/Event.
Toutes les notices ont la même présentation, y compris les anciennes catégories.
Les demandes et récompenses restent facultatives et descriptives : après le
texte viennent Request and rewards, son résumé, puis Publish. Le formulaire
Gazette place la photo et Choose a screenshot en premier, avant le titre,
puis le texte et Publish.
Photo obligatoire, brouillons, édition, retours des sélecteurs, suivis et
validations serveur restent actifs.
Le contrat fichier/MariaDB et le protocole ne changent pas. Les nouvelles notices
gardent la valeur compatible `info` ; éditer une ancienne annonce conserve sa
catégorie enregistrée sans l'afficher. La lecture n'entraîne aucune réécriture
des posts. Aucune migration, génération ou opération d'inventaire n'est ajoutée.
Les anciennes traductions de catégories restent disponibles pour les données
historiques. Titres ajoutés en français/anglais : Communauté/Community,
Aventure/Adventure et Partie/Game ; les autres libellés existent déjà.
## Vérifications et livraison
`./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
-PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan`
Réussite en 47 s (`PAUSE233_PASS`), après suppression de la barre sous Communauté.
Monde plat de développement, Survie sans Allow Commands, stockage fichier isolé.
- FR/EN, GUI 2/3/4, fenêtre 1280×960, avant et après Atlas : même ordre des
actions et des groupes, titres sans chevauchement, paire Discovery/Progression
centrée verticalement, navigation et options séparées.
- Ordre titre → texte → demande et résumé → publication, ou photo et sélecteur
→ titre → texte → publication ; aucune catégorie visible. Défilement du formulaire sur GUI
étroit vérifié. 38 captures dans `build/pause233-screenshots/`.
- Retours réels des sélecteurs : texte conservé, 128 bûches demandées et
récompense personnalisée publiées ; les 17 bûches et 3 diamants d'inventaire
réel restent présents. Photo toujours obligatoire, article et photo relus
depuis un nouveau lecteur du stockage fichier après publication.
- Ancienne catégorie `event` conservée lors de l'édition. Lecture des posts,
redimensionnements et changements de langue n'altèrent aucun octet du fichier
communautaire. Configuration du banc restaurée à la fin.
- Captures inspectées : pause verrouillée FR/2 et EN/4, pause déverrouillée FR/2,
notice configurée, Gazette avec photo assignée et bas du formulaire Notice Board
EN/4. Notices anciennes et nouvelles ont le même cadre et ne montrent que leur état.
Log client final : `build/pause233-client-vulkan-titled.log`. Aucun essai OpenGL ou Windows
et aucune migration MariaDB ne sont revendiqués ; le code serveur est conservé.
`./gradlew check build assemblePack -PsanctuaryFocusedTests=menus
-PsanctuaryAtlasOnly=true`
Réussite en 2 min 39 s : quatre GameTests requis, contrôles purs communautaires
(`COMMUNITY154_FILE_PASS`, `COMMUNITY157_PHOTOS_PASS`, `COMMUNITY158_TASKS_PASS`,
`COMMUNITY159_SEARCH_PASS`), build et assemblage du pack.
Log : `build/pause233-check-build-pack-final.log`.
Ce contrôle utilise le mode existant du banc pour les menus. Le chemin natif par
défaut présente toujours le conflit de générateur documenté en beta.232 ; cette
livraison ne revendique pas une nouvelle suite complète de génération.
Export local `build/Sanctuary-beta.233.mrpack` vérifié : intégrité ZIP, versions
Minecraft 26.3/Fabric Loader 0.19.5, un seul JAR Sanctuary embarqué, exactement
identique au JAR construit, sans module de test ni monde. Les 3 146 fichiers
de données et assets hors traductions sont identiques à beta.232 ; seules les
trois traductions de titres de groupes sont ajoutées en FR/EN, sans changer les
libellés existants. Reçu : `build/pause233-mrpack-receipt.json`.
- JAR : `23a148c197126ede552865f9847ce381700884d5ff087469a13e36540e8f23f9`.
- MRpack : `d95aeeca5f255db4444a22879f465477c0c6a2a111dd4e1c777b20944a283e2b`.
Livraison locale beta.233 ; aucune installation personnelle, publication
distante, ouverture d'un monde personnel ou mise à jour du canal packwiz.
+115
View File
@@ -0,0 +1,115 @@
# WG-215 — Relais régionaux visibles
Branche `codex/regional-relays-beta215`, depuis beta.214 (`b82ab37`).
## Contrat
Retirer le village Far West des nouvelles générations. Au Sud et au Nord,
remplacer le petit marqueur de relais par un monument compact, massif et
surélevé, apparenté aux ancres de Sanctuary, avec des matériaux régionaux.
La rosace allumée du relais reçoit entièrement la couleur de sa direction ;
le bloc originel conserve sa progression indépendante par huit gemmes.
Agrandir modérément la pyramide extérieure, sans changer le labyrinthe,
les coffres ou les pièges validés.
Nouveaux profils 215 uniquement ; les profils historiques gardent leur recette.
Aucun ancien monde ou chunk n’est retouché, aucune conversion de journal.
Les textures communes d’ancre changent d’aspect, sans changement de sauvegarde.
Essais autorisés sur des mondes jetables neufs : activation réelle des ancres
Nord/Sud, interruption et reprise. Nouvelle visite Vulkan 32 chunks et MRpack
local ; aucun canal public ou Prism modifié. Graines prévues : 42 et 0.
## État
Implémenté ; essais natifs et assemblage final réussis. Nouvelle visite ouverte.
Le rendu reste à apprécier en jeu.
## Réalisation
`RegionalRelay215` ajuste un socle octogonal de 13 blocs de large à une surface
rocheuse mesurée (feuilles et eau exclues). Recherche limitée à 64 blocs autour
du relais, dénivelé maximal de quatre blocs sous le socle, supports courts.
L’ancre est cinq blocs au-dessus de la base, accessible par un escalier de
largeur trois ; deux pylônes massifs encadrent la direction extérieure.
Améthyste sous le verre, tuff sculpté, éclairages limités. Au Sud, terracotta
de la strate locale et verre rouge ; au Nord, diorite, calcite, glace compacte
et verre bleu clair. Pas de toit masquant le bloc.
Plan immuable indexé par chunk, réservé avant végétation, compris dans les
chunks préparés avant l’annonce SGA. Le manoir du Nord évite cette emprise.
Le paiement du relais se réfère à la position surélevée. Aucun calcul permanent
ou nouveau format de sauvegarde. Les autres directions conservent leur recette.
Le village Far West est limité à son ancien profil 214.
Pyramide 215 : base 31 × 31 au lieu de 23 × 23, sommet quatre blocs plus haut,
même terracotta locale et entrée prolongée. Le gabarit souterrain, les dix
coffres et les huit pièges (24 TNT) sont conservés. La recherche contrôle toute
la nouvelle base ; une courte fondation raccorde la coque à la roche.
Les huit textures de relais allumés colorent toutes les gemmes d’une seule
teinte directionnelle, échantillonnée au cœur de chaque gemme de l’illustration.
Logo central, transparence et état éteint conservés. Les 514 ressources du
bloc originel (textures/modèles) sont comparées à l’artefact 214 ; sa progression
par gemmes reste indépendante.
## Essais natifs
Mondes jetables `south215-verified` (graine 42) et `north215-verified0` (0),
île principale Small 512, expansion de 1024. Paiement de l’ancre racine, rejet
d’un mauvais paiement et d’un doublon, interruption après réservation puis
reprise réussis. Aucun ancien monde utilisé.
| Relevé | Sud — 42 | Nord — 0 |
| --- | --- | --- |
| Ancre surélevée | 128, 258, 1224 | 352, 211, -1252 |
| Chunks du monument | 4 | 4 |
| Blocs comparés au plan | 2326 | 2390 |
| Profondeur maximale des supports | 6 | 8 |
| Marches d’approche | 5 | 14 |
| Chunks préparés | 67 | 58 |
Tous les blocs du monument, y compris les dégagements et les marches,
correspondent aux chunks produits. Le paiement incomplet sur la nouvelle
position est refusé avec `required`, confirmant que le relais est reconnu
(et non `invalid`). Reconstruction du cache identique après reprise.
Le village 214 n’est ni actif ni planifié dans ces profils.
Au Sud, pyramide en (32, 249, 848), largeur 31, dix coffres, dix piles
enchantées et 24 TNT vérifiées. Au Nord, le manoir reste planifié, séparé
du monument (origine 364, 255, −635). Pas de nouvelle passe de relief.
Ces essais sont fonctionnels : aucun benchmark, test Windows ou jugement
esthétique n’est déduit de leurs temps de génération.
## Vérifications et livraison locale
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south213,relays215 -PsanctuaryAtlasOnly=true`
réussi en 3 min 56 s : contrôles purs et **10 GameTests ciblés**, journal
`build/relays215-final-build.log`. La suite historique complète des GameTests
n’est pas revendiquée. Tests de placement Nord/Sud sur graines directes 0 et 42,
accès à pied, supports bornés, palette et reconstruction déterministe ;
contrôle des anciens profils et de l’intérieur de la pyramide.
La première sélection des teintes pouvait choisir le même reflet clair pour
deux secteurs : corrigée par un échantillonnage des huit cœurs de gemmes.
Contrôle final : huit images distinctes, 103 pixels colorés chacune,
logo central et alpha préservés (`build/relays215-textures.log`). Les trois
fichiers de bruit de l’île principale sont identiques à 214.
MRpack normal : `/Users/koka/Downloads/Sanctuary-beta.215.mrpack`.
Intégrité ZIP, métadonnées, JAR exact, modèles et textures corrigées, trois tailles
et profils historiques contrôlés ; aucun module Test ou monde inclus.
SHA-256 : `8fb19d272fa56b47ac15e79c4ecdba8fcff784280bef0961c8c68c592aa3ba02`.
Reçu : `build/relays215-mrpack-receipt.json`. Aucun tag, canal public ou Prism
modifié. Une graine dont le terrain ne permet aucun site dans la recherche
bornée peut encore refuser le monument ; les quatre placements du test et
les deux expansions réelles ci-dessus passent.
## Visite
Solo neuf `Sanctuary-South-215-42`, ouvert le 5 octobre 2026 à **07:49:47 CEST**,
confirmé par `SOUTH215_VISIT_OPEN`. Départ (150.5, 269, 1200.5), face au relais
(128, 258, 1224). Pyramide en (32, 249, 848). Créatif en vol, commandes,
32 chunks / simulation 5. Backend Vulkan (MoltenVK 1.4.2) confirmé dans
`build/south215-launch.log` ; aucun essai OpenGL. Ancienne visite 214 arrêtée
proprement et conservée. Le joueur reste juge du rendu des monuments.
+114
View File
@@ -0,0 +1,114 @@
# SOLO-232 — réglages et progression en solo
Branche `codex/solo-time-building-vein`, base beta.231, Minecraft 26.3.
## Comportement et contrat de sauvegarde
`/sanctuary time timezone <fuseau IANA>` choisit le fuseau et sa ville solaire
de référence pour ce monde. `timezone auto` reprend le réglage automatique
solo ou la configuration dédiée. Le propriétaire solo peut faire ce choix
sans Allow Commands ; les autres joueurs et le serveur dédié exigent les
droits opérateur. Le statut affiche le fuseau et la commande propose les
identifiants disponibles sans accès réseau.
Le choix persiste dans le champ opaque `reference` du contrat solaire existant,
avec le préfixe `timezone:`. Le schéma 1 et les autres champs restent inchangés.
Les anciennes références restent lisibles. Le changement réancre le calendrier
à son cycle courant, écrit atomiquement avant l'application solaire et ne
change jamais `gameTime`. Le mode Vanilla reste Vanilla. `auto` retire le
préfixe ; une ancienne version reprendrait sa référence configurée au prochain
rechargement. La configuration globale et les autres mondes ne sont pas modifiés.
Seule la commande explicitement exécutée par le joueur change son contrat.
Construction utilise les portées 3 / 3,5 / 4 / 4,5 / 5 / 5,5 / 6 / 7 blocs.
Avec un bloc en main principale ou secondaire, la visée suit cette portée :
un contour hors portée native reste un support de pose. Minage, contenants,
combat, protections, collisions et stock conservent leurs validations propres.
Une pose visible peut être refusée pour ces raisons ordinaires, mais pas par
une portée de construction différente de celle de la visée.
R + clic gauche maintenus terminent le groupe verrouillé, puis sélectionnent
le nouveau bloc regardé et démarrent un nouveau groupe. Chaque groupe conserve
sa durée native, son coût et la validation serveur. Une confirmation serveur
de réussite permet la reprise ; refus, outil cassé ou remplacé, interruption,
menu ou relâchement arrêtent le geste. Un maintien ne rejoue jamais le même
groupe terminé. Aucun groupe n'est lancé sans cible valide.
## Menu pause et butin de l'île
Avant l'achat Atlas, aucun widget World Map ni requête
de carte n'est créé dans le menu pause. Discovery, Progression, Inhabitant,
Faction, Battle, Options, World Options et Save and Quit sont centrés.
Gazette est à gauche des boutons et Notice Board à droite, chacune sur toute
la hauteur du corps du menu. Calendrier et bulletin conservent leurs emplacements.
Après déblocage, la disposition avec carte
et les deux fils à droite revient à la prochaine ouverture du menu.
Les diamants sont retirés lors du premier remplissage
des coffres et wagonnets à coffre situés sur l'île initiale du générateur
Sanctuary, quelle que soit la table de butin (mines natives comprises).
Les expansions, autres dimensions et mondes ordinaires conservent leurs tables.
Les coffres déjà ouverts et les objets déposés manuellement sont conservés.
Les caches non ouvertes de mondes existants suivent cette nouvelle règle à leur
ouverture ; aucune visite ou réécriture de chunks n'a lieu. Aucun minerai ni
profil de génération ne change. La graine n'affecte pas l'exclusion des diamants.
La règle s'applique au générateur courant NoiseBased et à ses trois tailles,
ainsi qu'aux anciens profils Sanctuary. Le filtre intercepte uniquement
`LootTable.fill`, sans changer les tables elles-mêmes ni leurs autres usages.
Les 794 ressources de génération et de butin du JAR beta.231 restent identiques.
La génération demeure 231 ; graines de butin vérifiées : 0, 42, 232, puis
256 tirages de la table native abandoned_mineshaft.
## Livraison et vérifications
Version mod et pack : `beta.232`, Minecraft 26.3, Fabric Loader 0.19.5, Java 25.
Le nouveau message serveur `sanctuary:mining_result` autorise la reprise d'un
groupe terminé ; client et serveur doivent utiliser cette livraison ensemble.
Le client attend aussi la mise à jour du bloc initial avant de recalculer sa
visée, car la confirmation peut arriver avant les blocs détruits.
`./gradlew check build assemblePack -PsanctuaryFocusedTests=realtime,mining,building
-PsanctuaryAtlasOnly=true` réussit en 2 min 36 s : 24 GameTests requis et les
contrôles purs du dépôt passent. Fuseau persistant/idempotent, fuseau invalide
et contrat modifié extérieurement refusés, mode Vanilla et horloge native
préservés ; validations serveur du minage et de la construction conservées.
Log : `build/solo232-check-build-pack.log`.
`./gradlew :sanctuary:runGameTest -PsanctuaryFocusedTests=solo232
-PsanctuaryTestGeneration=13` réussit en 37 s : 2 GameTests requis, remplissage
réel avec le mixin, conservation des diamants déposés et du fer, butin distant
et contexte absent préservés, identification des trois profils courants.
Log : `build/solo232-loot-native.log`.
Les contrôles utilisent les modes existants du banc d'essai. Son chemin par
défaut tente encore de convertir le preset courant NoiseBased en ancien
SanctuaryChunkGenerator et échoue avant les tests. Ce ticket ne corrige pas
ce banc historique et ne revendique pas une suite complète de génération.
`./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
-PsanctuarySolo232ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan`
réussit en 1 min 3 s, Vulkan confirmé avec MoltenVK 1.4.2. Un propriétaire
Survie sans Allow Commands utilise réellement la commande de fuseau. Deux
groupes successifs sont minés avec R + clic gauche maintenus, huit destructions
et huit usages natifs d'outil ; un outil cassé interrompt le geste après une
destruction sans redémarrer. L'essai Balance080 couvre aussi pose native aux
deux mains, portée initiale, contour et pose à 6,75 blocs, refus d'un paquet
forgé à 7,25 blocs, indépendance Minage/Construction, contenants, nénuphars,
construction groupée et bonus externes.
Le menu pause est contrôlé avant/après achat Atlas, en FR/EN, aux échelles
GUI 2/3/4 dans une fenêtre 1280×960 : huit actions présentes et dans le corps,
colonnes sans chevauchement, carte uniquement après achat. Douze captures
sont conservées dans `build/solo232-screenshots/`, avec inspection visuelle
du mode verrouillé FR/2 et EN/4, et du mode débloqué FR/2. Les libellés longs
gardent les infobulles natives aux petites largeurs.
Log : `build/solo232-client-vulkan-final.log`, marqueurs `SOLO232_PASS` et
`BALANCE080_PASS`. Aucun essai OpenGL ou Windows n'est revendiqué.
MRpack local : `build/Sanctuary-beta.232.mrpack`, intégrité ZIP, versions,
dépendances exactes, JAR construit identique, absence de monde et d'entrée
Sanctuary Test vérifiées. Reçu : `build/solo232-mrpack-receipt.json`.
SHA-256 pack : `03e588c3866ad513d057162e9fea4ba9ad07e33a522841e2e7bbd66356b153f8`.
SHA-256 JAR : `a208ccce6318ceccf04d35d983e4a9ea1d46e872fda202d47a37000faa740d9f`.
Aucun monde personnel, installation de jeu ou canal packwiz n'est modifié.
+120
View File
@@ -0,0 +1,120 @@
# WG-209 — premier Sud, canyons multicolores
Branche `codex/south-badlands-beta209`, depuis beta.208 (`d021341`).
Le créateur valide le Nord 208 et demande ce premier prototype jouable du Sud.
## Contrat
Expansion Sud de diamètre 1024 : réseau de vallées creusées par Voronoi inversé,
mesas à plusieurs altitudes, falaises et petites ouvertures par bruit 3D.
La forme et la matière sont indépendantes. Les seize terracottas forment des
strates continues, plissées et marbrées ; leur disposition varie avec la graine.
Poudre de béton assortie sur les surfaces portantes, avec du sable rouge.
Pas de partage fixe en trois secteurs ni de roue chromatique autour de l'île.
Pas de nouvelle structure à cette étape : le test porte sur le relief et la matière.
Nouveaux profils 209 uniquement ; terrain de Sanctuary et Nord 208 conservés.
Tous les profils historiques restent disponibles, sans migration. Contrat de
test : nouveaux mondes de développement, graines 42 et 0, activation réelle de
l'ancre Sud et interruption/reprise de sa préparation. Nouvelle visite solo
Vulkan à 32 chunks, sans retoucher une sauvegarde existante. Export MRpack local ;
aucune publication de canal ni mise à jour Prism.
## Réalisation du prototype
- Sites Voronoi décalés par la graine, domaine déformé à grande échelle.
La distance aux frontières creuse un réseau ramifié ; sa largeur et sa
profondeur varient. Mesas à différentes altitudes, petits replats sur certains
flancs, rares creux sur les tables et alcôves en bruit 3D. Pas de simulation
globale d'érosion ou d'hydrologie. Les colonnes sont mises en cache par worker.
- Champ de matière indépendant : couches de sept blocs, plissées par deux
échelles horizontales et un bruit 3D. Les seize teintes sont groupées en
familles dont l'ordre et le sens varient avec la graine, sans secteurs radiaux.
- Une couche de poudre de béton assortie sur les surfaces peu inclinées et
soutenues ; plages de sable rouge. La terracotta constitue les parois et le
support. Végétation sèche native parcimonieuse, aucun lac ni structure ajouté.
L'économie des minerais et les temples du Sud restent pour la suite.
- Ancre Sud et relais suivants : climat aride, relief canyon et diamètre 1024 ;
petit éventail directionnel conservé. Préparation de 49 chunks d'arrivée et
neuf près du relais ; exploration progressive du reste. Signal SGA FR/EN.
- Presets 209 Small/Medium/Large et défaut Sanctuary. Densités de l'île principale
identiques aux fichiers 208 ; Nord délégué aux mêmes classes 208.
## Vérifications
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south209 -PsanctuaryAtlasOnly=true` :
succès en 5 min 43 s, contrôles purs et huit GameTests ciblés passés.
Journal : `build/south209-check-build.log`. Le Sud est contrôlé sur trois graines
pour ses vallées réparties, mesas, hauteurs, alcôves, appuis et déterminisme
entre workers. La palette conserve seize couleurs et une continuité spatiale ;
les anciens profils restent résolubles et les régressions du Nord passent.
La suite historique complète des GameTests n'est pas revendiquée.
Le premier essai natif a révélé un cycle d'ordre de décoration entre
`wooded_badlands` et le nouveau biome. La liste locale plaçait les buissons morts
avant l'herbe sèche ; elle reprend désormais l'ordre natif. Un GameTest charge
la liste complète des biomes de chaque taille et vérifie son tri topologique.
Reconstruction avec `:sanctuary:runGameTest build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch -x :sanctuary:check`, même sélection ciblée :
succès en 47 s, neuf GameTests passés. Les contrôles purs précédents ne sont pas
répétés pour cette correction de ressource. Journal : `build/south209-order-fix.log`.
Les deux essais avortés sont conservés ; les essais finaux utilisent de nouveaux
répertoires `south209-b` (42) et `south209-b0` (0).
Les premiers essais corrigés `south209-b` et `south209-b0` passent : activation
réelle, reprise 0/58 → 58 FULL, seize couleurs, sables soutenus et biomes cohérents
dans les colonnes inspectées. Une revue finale limite explicitement ce champ au
diamètre 1024 : les commandes opérateur de plus petite taille gardent le canyon
historique. Le petit socle du relais descend jusqu'à six blocs si un vide local
le sépare du terrain. Un contrôle natif vérifie ses neuf appuis.
Reconstruction finale avec la même commande ciblée : neuf GameTests passés,
succès en 34 s (`build/south209-final-build.log`). Les essais finaux sur
`south209-final` (42) et `south209-final0` (0) passent également sur le JAR final.
Chaque serveur s'arrête proprement après vérification. Paiement réel, refus de
l'offrande incorrecte et du doublon, interruption à 0/58, reprise à 58 chunks FULL,
relais orienté Sud et message SGA vérifiés.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Hauteurs sur la grille intérieure | 147–289 | 144–297 |
| Échantillons de vallée / mesa | 618 / 677 | 568 / 703 |
| Blocs inspectés dans les colonnes natives | 17 991 | 11 525 |
| Couleurs de terracotta observées | 16 | 16 |
| Sables inspectés / sans support | 89 / 0 | 120 / 0 |
| Colonnes avec biome attendu | 150 / 150 | 150 / 150 |
Ces relevés sont des échantillons, pas un inventaire de l'île entière. Les essais
simultanés ne constituent pas un benchmark de performances. Aucun essai Windows
ni validation esthétique n'est déduit de ces contrôles.
## Distribution et visite
`build/Sanctuary-beta.209.mrpack`, copié dans `~/Downloads/`. Archive ZIP intacte,
JAR identique au build, presets 208 et 209 des trois tailles présents, aucun
module Test ni monde embarqué. Minecraft 26.3 / Fabric Loader 0.19.5.
Reçu : `build/south209-mrpack-receipt.json`. SHA-256 :
`e077ce64f59330c4916408d0ff79f8d0e445bcfb3a972953680ccc93c28392b7`.
Aucun canal public, serveur personnel ou instance Prism mis à jour.
Visite neuve `Sanctuary-South-209-42`, copie de l'essai final arrêté, dans
`build/worldgen-lab/south209-visit/small/42/client/saves/`. Vue 32 chunks,
simulation 5, créatif en vol et commandes. Position initiale :
**(-167.5, 323.13, 585.5)**, regard vers **(-112, 304, 656)**.
Arrivée du Sud : **(128, 258, 664)** ; relais : **(104, 241, 1248)**.
L'ancienne visite 208 a été arrêtée par son hook de sauvegarde natif ; sa
sauvegarde et les anciennes visites sont conservées.
## Limites du prototype
Relief et palette à apprécier en jeu. Pas encore de temple, de défi ou de
nouvelle distribution de minerais propre au Sud. Les couches et affleurements
colorés sont présents ; des gisements spéciaux distincts restent une piste.
Le rendu et le coût d'exploration à 32 chunks ne sont pas validés par les mesures
de préparation du relais.
Le client confirme Vulkan sur Apple M1 et `SOUTH209_VISIT_OPEN` le 5 octobre
à 04:26:12 en **(-168, 323, 585)**. Vue 32 / simulation 5 confirmées. Le journal
indique ensuite le passage du joueur en spectateur ; cela confirme l'accès à la
visite, sans constituer une validation esthétique du prototype.
+93
View File
@@ -0,0 +1,93 @@
# WG-213 — Pyramide camouflée et chambres enterrées
Branche `codex/south-buried-beta213`, depuis beta.212 (`3941bd4`).
## Contrat
R017 valide le relief 212, les récifs relevés et les couleurs radiales. Retirer
seulement les sables normal / rouge de surface et les acacias custom du Sud.
Remplacer le complexe extérieur par une pyramide de terracotta locale, une
descente, environ dix petites salles dans le sol, couloirs étroits et pièges.
La taille du complexe vient du nombre de pièces. Le site peut être ailleurs
que le centre. Aucun autre changement de paysage ou chantier de jeu.
Nouveaux profils 213 uniquement : aucun monde existant modifié ou régénéré.
Générateur `SouthRelief212` repris sans changement, même graine et bruit ;
palette 212 conservée. Le Nord et l'île de départ gardent leur génération.
Contrat de vérification : création neuve sur 42/0, ancre réelle, reprise,
inspection native et nouvelle visite Vulkan 32 chunks ; MRpack local.
Aucun canal public, serveur personnel ou instance Prism modifiés.
## Réalisation
- Le sol conserve la terracotta et ses petites zones de terre stérile ; aucun
sable ou acacia ajouté par le nouveau profil Sud. Herbes / buissons secs conservés.
- Pyramide extérieure de 23 blocs de côté et 10 blocs au-dessus du sol, palette
résolue sur les vraies strates de ses coordonnées. Deux niveaux de cinq salles,
sols à 16 et 30 blocs sous l'entrée ; intérieurs 5 × 5, hauteur libre 3.
- Escaliers et couloirs de deux blocs de large. Dix coffres avec le butin déjà
vérifié en 212. Huit poches de trois TNT, déclenchement par plaque ; pas de
nappe explosive continue. Les pièces sont creusées dans la roche et bordées
d'une enveloppe mince, les anciennes halles / pylônes / arches sont absents.
- Recherche déterministe dans l'île : couvre les plafonds des salles et préfère
une masse rocheuse assez pleine. Tolère des fissures naturelles localisées.
Placement limité au chunk courant, préparation inclut le nouveau complexe.
## Vérifications de code
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south212,south213 -PsanctuaryAtlasOnly=true`
réussi en 2 min 37 s, contrôles purs et **12 GameTests** passés
(`build/south213-final-build.log`). La première passe avait trouvé un accès
bloqué sous la première descente : un petit contournement dessert maintenant
les chambres du premier niveau (`build/south213-check-build.log`).
Comparaison exacte des densités et palettes 212/213 sur deux graines : relief
et couleurs identiques. Les trois fichiers de bruit de l'île principale sont
identiques octet pour octet. Contrôles des profils historiques, du Nord et de
l'ancien Sud. Parcours des deux escaliers, accès aux dix chambres et coffres,
poches de TNT séparées ; test natif d'amorçage de la plaque conservé dans 212.
La suite complète des GameTests historiques n'est pas revendiquée.
## Essais natifs et distribution
Nouveaux mondes `south213-final` (42) et `south213-final0` (0), départ Small et
expansion Sud 1024. Paiement réel de l'ancre, mauvais paiement / doublon refusés,
interruption puis reprise, relais et signal SGA vérifiés. La préparation inclut
la pyramide et son sous-sol. Les deux serveurs sont arrêtés proprement avant
la copie de la visite ; aucune modification des visites précédentes.
| Relevé borné | Graine 42 | Graine 0 |
| --- | --- | --- |
| Hauteur maximale sondée (identique à 212) | 301 | 304 |
| Colonnes rehaussées (identiques à 212) | 281 | 211 |
| Sable normal / rouge | 0 | 0 |
| Troncs / feuilles d'acacia | 0 / 0 | 0 / 0 |
| Faces froides colorées exposées | 1485 | 1418 |
| Coffres / piles enchantées | 10 / 10 | 10 / 10 |
| TNT dans huit poches distinctes | 24 | 24 |
| Roche préexistante sondée autour des pièces | 100 % | 95,56 % |
| Base de la pyramide | 32, 249, 848 | −112, 249, 944 |
Le rendu de chaque bloc de terracotta de la pyramide correspond à la palette
locale. Les plafonds des pièces sont sous le toit du terrain et leur espace
libre est contrôlé dans les chunks réels. Ces mesures sont des échantillons,
pas un inventaire de toute l'île ou un benchmark. Aucun test Windows ni
validation visuelle du joueur n'est revendiqué. La petite structure a un plan
fixe pour cette itération ; son emplacement, orientation et teinte varient.
MRpack normal exporté, ZIP intact, JAR exact et presets des trois tailles
212/213 vérifiés, aucun monde ou module Test inclus. Copie dans
`~/Downloads/Sanctuary-beta.213.mrpack`. Minecraft 26.3, Fabric Loader 0.19.5.
SHA-256 : `6d11e398a09f6f65c963eb022bc0482906a8126e0c4f67ffbee867cadbe1340c`.
Reçu : `build/south213-mrpack-receipt.json`. Aucun tag ou canal public publié.
Nouvelle visite `Sanctuary-South-213-42`, sous
`build/worldgen-lab/south213-visit/small/42/client/saves/`, créatif en vol,
commandes, Vulkan, 32 chunks / simulation 5. Départ **(31.5, 263, 810.5)**,
face à l'entrée **(32, 250, 837)**. Relais **(128, 251, 1224)**.
Client ouvert à **06:32:38 CEST le 5 octobre 2026** : `SOUTH213_VISIT_OPEN`,
monde et position attendus. Backend Vulkan et options 32/5 confirmés.
Le rendu reste à apprécier par le joueur.
+137
View File
@@ -0,0 +1,137 @@
# WG-216 — Forêts et pièges du Sud
Branche `codex/south-forest-beta216`, depuis beta.215 (`03aab49`).
## Contrat
Retirer les torches de la pyramide, disperser davantage de plaques de pression
reliées à des pièges dans les salles et passages, conserver l’architecture et
le butin validés. Planter de grandes forêts denses sur les nappes de terre du
Sud, sans réintroduire les acacias rejetés. Choix de cette passe : chênes
ramifiés natifs avec des hauteurs variées et une canopée continue.
Le joueur considère le Sud terminé pour le moment après ces ajustements.
Nouveaux profils 216 uniquement, aucune ancienne sauvegarde ou chunk retouché.
Relief, couleurs, Nord et monuments 215 conservés. Mondes jetables neufs sur
42 et 0 : activation Sud, interruption et reprise autorisées pour les essais.
Nouvelle visite Vulkan 32 chunks et MRpack local, aucun canal public ni Prism.
## État
Implémenté ; build, huit GameTests ciblés et essais natifs sur 42/0 réussis.
MRpack exporté et vérifié ; nouvelle visite ouverte. Rendu à apprécier en jeu.
## Complément R022
Uniquement des arbres classiques, aucun acacia. Ajouter des chemins de terre
sur la surface de l’expansion, répartis entre les lieux et secteurs boisés.
Implantation sur le relief existant ; détours autour des failles, pas de pont
ou terrassement massif ajouté par cette passe.
Palette R023 : 14 teintes dans 8 couronnes, conservées sur choix explicite du joueur.
## Réalisation
La pyramide conserve ses dix salles, ses coffres et son enveloppe de 31 × 31.
Le gabarit 216 retire les torches et répartit des plaques dans les chambres et
les couloirs ; chacune repose sur un bloc plein, avec une charge TNT cachée
en dessous. Les anciens gabarits conservent leur éclairage et leurs pièges.
Les chênes utilisent le générateur ramifié natif de Minecraft 26.3, avec un
tronc de hauteur variable (paramètres 8 + aléas 7/3) et 18 essais de plantation
par chunk. Plantation uniquement sur les nappes existantes de terre, terre
stérile ou herbe ; réserve autour de la pyramide, du relais, de l’arrivée et
des chemins. Aucun acacia ni changement des couleurs de terracotta.
Les chemins sont calculés une fois par expansion, sur une grille de 16 blocs
avec vérification du sol à chaque pas. Dix destinations couvrent le relais,
la pyramide et les huit secteurs de l’île. La recherche privilégie les détours
praticables, puis peint des sentiers de trois blocs de large, resserrés sur
les bords. Les plateaux séparés par le vide ont des réseaux locaux distincts :
aucun pont ni remblai ne les raccorde artificiellement. Les destinations sont
approchées selon la praticabilité du terrain, sans garantie de liaison complète
entre tous les monuments. Plan indexé par chunk ; aucune prégénération de
l’île entière, aucun recalcul à chaque tick.
Les profils 216 Small/Medium/Large et le biome `sanctuary:south_badlands216`
isolent cette évolution. Les reliefs, matériaux et anciens profils restent
lisibles. Le nouveau biome n’ajoute que le placement des chênes.
## Vérifications
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,relays215,south216 -PsanctuaryAtlasOnly=true`
réussit : contrôles purs et huit GameTests ciblés. Plaque avec déclenchement
réel d’une TNT enterrée, accès aux dix chambres, conservation des anciens
profils et des relais. Planification directe : dix branches et 4 962 cellules
sur la graine 0 ; dix branches et 5 230 cellules sur 42. Parcours vérifiés
bloc par bloc, reconstruction identique du plan. Ces graines directes diffèrent
des graines dérivées utilisées lors d’une activation depuis Sanctuary.
Comparaison des ressources : bruit de terrain identique à 215 pour les trois
tailles ; préréglages modifiés uniquement pour leur révision et le biome 216.
Code de sélection des couleurs strictement identique : 14 teintes, 8 couronnes.
Les premiers essais ont décelé une recherche de sentiers bloquée dans un petit
secteur isolé. La planification distingue désormais les plateaux reliés à pied
et produit des réseaux locaux, sans franchissement artificiel du vide.
## Mondes générés et reprise
Essais finaux `south216-final` (42) et `south216-final0` (0), après correction
du relevé des arbres : les branches horizontales touchant une paroi étaient
comptées à tort comme des racines. La règle de plantation n’a pas changé ;
le relevé contrôle désormais les troncs verticaux. Le premier contrôle natif
avait aussi exigé une pyramide sur 0 alors que la recette conservée n’y trouve
pas de site enterré. Ce cas est relevé explicitement, pas converti en réussite
de placement. Le contrôle exige toujours la pyramide sur 42.
Activation par l’ancre Sud, paiement, rejet d’un doublon, interruption et
reprise réussis dans les deux mondes. Île principale Small 512, expansion de
1024 centrée en (128, 944). Graines dérivées : −6763683964522674921 (42) et
3285501279717306940 (0).
| Relevé natif | 42 | 0 |
| --- | ---: | ---: |
| Branches de sentiers | 10 | 9 |
| Cellules de chemin prévues | 3 509 | 2 106 |
| Blocs de chemin vérifiés dans les chunks | 470 | 478 |
| Chunks de chemin contrôlés | 10 | 9 |
| Troncs dans la zone forestière de 49 × 49 | 69 | 97 |
| Chênes avec tronc d’au moins 8 blocs | 61 | 91 |
| Colonnes couvertes par les feuilles | 1 657 | 2 177 |
| Blocs de feuilles | 13 210 | 17 054 |
| Acacias dans le relevé | 0 | 0 |
Chemins soutenus et deux blocs libres au-dessus ; troncs verticaux sur terre.
Les monuments des relais sont comparés au plan bloc par bloc (2 326 / 2 217
blocs), avec accès et paiement sur leur position surélevée.
Pyramide de 42 en (32, 249, 848) : 10 coffres, 10 piles enchantées,
40 plaques de pression, 56 TNT et **aucune torche**. Les galeries restent
accessibles avant déclenchement. Sur 0, aucune pyramide : limite du placement
215 conservé, qui refuse un massif insuffisamment plein. Les routes n’assurent
pas une liaison continue à travers toutes les failles. Ces essais ne sont
ni un benchmark, ni une validation graphique du rendu, ni un essai Windows.
## Distribution locale
`build/Sanctuary-beta.216.mrpack`, également copié dans Downloads, contient le
mod normal avec les trois tailles ; aucun module Test ni monde. Intégrité ZIP,
version, profils historiques et identité exacte du JAR construit vérifiés.
SHA-256 MRpack : `e5c9c6bd9a1a4602f290732a9adf49652c217ab4858db256e3cfe6d8ee1f2bf3`.
SHA-256 JAR : `0807809aa1e8c889cb84e56de99e24f8b3702dd72bba5e56f809bbb4854f23d2`.
Après les corrections de diagnostic uniquement, le module Test a été recompilé
et réassemblé ; le JAR principal reste exactement celui du `check build` réussi.
Reçus ignorés : `build/south216-static-receipt.json`,
`build/south216-mrpack-receipt.json` et rapports `warm.json` des deux essais.
Nouvelle sauvegarde de visite : `south216-visit-final`,
`Sanctuary-South-216-42`, copie neuve du monde vérifié, créatif avec commandes,
vue 32 chunks et simulation 5. Départ face à la forêt en (284, 204, 1170),
forêt en (256, 172, 1200), relais en (128, 258, 1224).
Aucun ancien monde modifié ; aucun canal public, tag ou Prism mis à jour.
Ouverture confirmée par `SOUTH216_VISIT_OPEN` le 5 octobre 2026 à 08:36:18 CEST,
position (284, 204, 1170), backend Vulkan confirmé dans le journal client.
+122
View File
@@ -0,0 +1,122 @@
# WG-210 — familles de teintes et sols des canyons
Branche `codex/south-palette-beta210`, depuis beta.209 (`efb1097`).
## Contrat
Le créateur accepte le relief du Sud 209 et demande une famille de teintes
terreuses par massif, des strates irrégulières moins contrastées, un fond de
canyon continu et de la végétation sèche dans les vallées et sur les plateaux.
Conserver strictement la densité, les dimensions, les alcôves et les relais.
Ni dégradé systématique sombre/clair selon l'altitude ni cycle des seize couleurs.
Nouveaux profils 210 uniquement ; les mondes et profils historiques restent
inchangés. Tests autorisés sur de nouveaux mondes de développement, graines 42
et 0 : activation de l'ancre Sud, interruption/reprise, contrôle du terrain
natif et nouvelle visite Vulkan à 32 chunks. Export MRpack local. Pas de
migration de sauvegarde, publication de canal ou déploiement Prism.
## État
Implémentation compilée ; contrôles ciblés et essais natifs 42/0 réussis. Le jugement esthétique
reste à apporter en visite ; les contrôles techniques ne valident pas le rendu.
## Réalisation
Le champ `SouthRelief209` conserve ses calculs de densité. Il expose seulement
l'identifiant du massif Voronoi déjà choisi et le fond théorique de vallée.
`SouthTerrain210` réutilise ce champ et les sites d'arrivée/relais 209.
`SouthPalette210` prépare quatre familles apparentées : rouille, ocre, terre,
crème. Pour chaque massif, une roche dominante couvre l'essentiel du volume ;
des lits secondaires de 2–6 blocs et des filets de 1–2 blocs interrompent des
espacements de 8–26 blocs. Les piles sont indépendantes, déterministes par graine
et légèrement plissées. Le pied des massifs reste en terracotta commune.
Le cycle des seize teintes et les poudres colorées restent propres au profil 209.
Les fonds plats reçoivent un sol commun de terre stérile, terre et sable rouge,
piloté par un bruit continu indépendant des cellules. Les plateaux reçoivent des
plages de sol et conservent des affleurements. Le sable repose sur du grès rouge.
Cette passe remplace les blocs solides existants ; elle ne creuse ni ne remplit
les vallées. La continuité porte sur le sol, aucune rivière n'est ajoutée.
L'herbe sèche courte/haute, les buissons morts et quelques cactus sont placés
sur les surfaces portantes des vallées et des plateaux. Les règles natives de
support sont vérifiées. Les candidats restent à l'intérieur de leur chunk et
ne lancent aucune recherche de terrain supplémentaire. Le biome 210 a une liste
de features vide : cette décoration locale évite d'ajouter deux fois les plantes.
Les trois tailles publiques basculent vers des profils 210 avec les mêmes
réglages de l'île principale ; le Nord conserve le champ accepté en 208.
Les profils 196–209 restent chargeables et ne changent pas de palette.
## Vérifications de compilation et de comportement
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south209,south210 -PsanctuaryAtlasOnly=true`
réussit en 2 min 49 s : contrôles purs et 11 GameTests ciblés passés.
Journal `build/south210-check-build.log`. La suite historique complète des
GameTests n'est pas revendiquée.
Les tests couvrent les densités et relais 209/210 identiques sur trois graines,
la dominante par massif, les quatre familles, l'irrégularité des couches, leur
continuité spatiale, le sol de vallée indépendant de l'identité du massif et
les sols des plateaux. Ils conservent les contrôles du Nord 208, de la palette
historique 209 et des trois tailles publiques avec leurs anciens profils.
Les trois JSON de densité de l'île principale sont identiques octet pour octet
aux profils 209 ; aucune migration n'est effectuée.
Contrôles natifs réalisés sur de nouveaux mondes 42/0 ; résultats ci-dessous.
Le premier essai 42 passe avec 1 493/1 493 colonnes de vallée éligibles couvertes
par le sol et des plantes sèches en vallée comme sur mesa. L'échantillon initial
0 n'observait que trois teintes dans les points d'arrivée, du relais et du canyon ;
il ne visitait pas les quatre familles. Le contrôle natif inclut désormais une
mesa représentative de chaque famille avant de compter les matériaux. Aucun
changement du générateur n'est effectué pour ce correctif d'échantillonnage.
Reconstruction ciblée : 11 GameTests passés en 17 s, journal
`build/south210-final-build.log`. Nouveaux essais `south210-verified` (42) et
`south210-verified0` (0), les premières traces étant conservées.
## Résultats natifs finaux
Les deux essais passent, avec ancre réelle, paiement, refus du mauvais objet et
du doublon, interruption à 0/58, reprise à 58 chunks FULL, neuf appuis de relais
soutenus et signal SGA. Les serveurs sont arrêtés proprement après contrôle.
Les hauteurs et sites de relais correspondent aux relevés 209 des mêmes graines.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Hauteurs intérieures | 147–289 | 144–297 |
| Colonnes avec biome attendu | 250 | 250 |
| Blocs natifs inspectés | 29533 | 24317 |
| Teintes observées parmi les quatre familles | 7 | 7 |
| Sables inspectés / sans support | 50 / 0 | 4 / 0 |
| Sol continu dans la vallée contrôlée | 1493 / 1493 | 1325 / 1325 |
| Plantes sèches, secteur vallée | 39 | 29 |
| Plantes sèches, secteur mesa | 45 | 18 |
Les relevés portent sur des échantillons, pas sur l'île entière. Les plantes
observées sont des herbes sèches et buissons morts ; les rares cactus prévus
n'apparaissent pas dans ces deux petits secteurs. Aucun benchmark, essai Windows
ou jugement esthétique n'est déduit des tests serveur.
## Distribution et visite
`build/Sanctuary-beta.210.mrpack`, copié dans `~/Downloads/`. Intégrité ZIP,
version, identité du JAR construit, trois tailles 209/210 et absence de mondes
ou de module Test vérifiées. Minecraft 26.3 / Fabric Loader 0.19.5.
Reçu `build/south210-mrpack-receipt.json`. SHA-256 :
`eb91fe9f037b52f5ac4ef273d45ee2ad2ce4e85b5b24870ed627ba1672d60ce5`.
Visite neuve `Sanctuary-South-210-42`, copie de l'essai 42 arrêté, dans
`build/worldgen-lab/south210-visit/small/42/client/saves/`. Vulkan, 32 chunks,
simulation 5, créatif en vol et commandes. Même départ que la visite 209 :
**(-167.5, 323.13, 585.5)**, regard vers **(-112, 304, 656)**.
Arrivée **(128, 258, 664)** ; relais **(104, 241, 1248)**.
Client ouvert à 05:02:17 CEST le 5 octobre 2026 : marqueur
`SOUTH210_VISIT_OPEN`, monde et position attendus, backend Vulkan confirmé.
Les anciennes visites sont conservées ; aucun canal public ni instance Prism
n'est modifié. Les temples et ressources propres au Sud restent hors de ce lot.
+109
View File
@@ -0,0 +1,109 @@
# WG-212 — Couronnes des Badlands et complexe du désert
Branche `codex/south-rings-beta212`, depuis beta.211 (`7375046`).
## Contrat et retours
R013 valide la base 211, critique le grand plateau et le cœur coloré invisible.
Rendre les couleurs concentriques visibles, ajouter du sable normal, de vrais
arbres secs et petits arbres feuillus, relever localement les plateaux.
R014–R016 précisent un grand complexe allongé, du butin et beaucoup de TNT.
La recherche d'une emprise parfaitement soutenue est abandonnée explicitement :
accepter encastrements et surplombs dans le terrain 3D, avec une architecture qui
les accompagne. Le complexe peut se décaler du centre.
Diamètre Sud 1024. Nouveaux profils 212 uniquement ; aucune migration ni
régénération des mondes antérieurs. Nord 208 et île principale conservés.
Contrat de test : nouvelles graines 42/0, activation réelle et reprise, contrôles
natifs, puis nouvelle visite Vulkan 32 chunks et MRpack local. Pas de publication
publique, de déploiement Prism ou de serveur personnel.
## Mise en œuvre
- Densité 211 conservée en profondeur, et couronnes de mesa localisées au-dessus
de roches hautes portantes. Données de cellules Voronoi 209 réutilisées pour le relief,
sans ses gradins ni son ancienne dalle. Les fissures 3D continuent dans les ajouts.
- Huit familles radiales légèrement déformées, visibles sur les toits et parois.
Les bandes natives de `MaterialSystem` choisissent les nuances de chaque famille.
- Désert central de sable normal / grès, nappes secondaires et végétation sèche.
Acacias dénudés ramifiés et petits arbres feuillus ; placement entier et borné
dans le chunk. Le temple réserve sa place avant décoration.
- Complexe de grès allongé, chambres latérales, galerie, sanctuaire haut et cache
dissimulée. Piliers courts et arches sous les parties suspendues. Six circuits
de pièges, 108 TNT et huit coffres : fer enchanté, provisions, minerais et gemmes.
Plan déterministe en mémoire, placement limité au chunk courant. Tous ses
chunks sont inclus dans la préparation de l'expansion avant le signal SGA.
## Vérifications de code
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south211,south212 -PsanctuaryAtlasOnly=true`
réussi : contrôles purs et 11 GameTests. Après optimisation du planificateur,
restriction des arches à l'emprise construite et ajout d'un test d'amorçage réel,
reconstruction avec `:sanctuary:runGameTest build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch -x :sanctuary:check`, même sélection : **12 tests
passés**, build réussi. Traces `build/south212-check-build.log`,
`build/south212-final-build.log`, `build/south212-release-build.log`.
Couverture : profils courants et historiques sans module Test, Nord accepté,
ancien Sud, cavités 211 conservées en profondeur, ajouts localisés, vides non
rebouchés par le relief, déterminisme entre workers, huit familles radiales,
complexe planifié sur deux graines, communication des pièces, contenu des pièges
et TNT réellement amorcée par une plaque via son sol. Les trois réglages de
bruit de l'île principale sont identiques octet pour octet à ceux de 211.
Les profils historiques et leurs Java restent inchangés. Le test complet de
code n'inclut pas tous les GameTests historiques ; sélection ciblée ci-dessus.
Le premier essai natif a révélé une amplitude négligeable des ajouts : le seuil
utilisé convenait au champ synthétique du test, mais pas aux densités du bruit
réel. Seuil corrigé, assertion ajoutée sur le générateur natif et nouvelle
reconstruction ciblée réussie, 12 tests (`build/south212-crowns-build.log`).
Les mondes du premier essai sont conservés séparément ; les mesures finales
proviennent de mondes neufs avec la bonne empreinte des sources.
## Essais natifs finaux et distribution
Mondes neufs `south212-final2` (42) et `south212-final02` (0), île de départ Small
et Sud 1024. Ancre réelle, refus du mauvais paiement et du doublon, interruption
0/86 puis reprise 86 chunks FULL, relais soutenu et signal SGA vérifiés. Les deux
serveurs sont arrêtés proprement avant de copier la visite.
| Relevé borné | Graine 42 | Graine 0 |
| --- | --- | --- |
| Hauteur maximale sondée | 301 | 304 |
| Ajout maximal théorique | 53,44 | 53,50 |
| Colonnes sondées relevées de plus de 8 blocs | 281 | 211 |
| Faces colorées froides adjacentes à l'air | 1423 | 1364 |
| Sable clair / sans support | 39 / 0 | 36 / 0 |
| Troncs / feuilles dans les colonnes sondées | 11 / 28 | 3 / 8 |
| Coffres / piles enchantées | 8 / 8 | 8 / 8 |
| Blocs TNT dans le complexe | 108 | 108 |
| Origine du complexe | 64, 250, 912 | 32, 250, 976 |
Les huit familles sont testées dans le code ; cinq teintes froides sont
observées exposées dans chacun des deux relevés natifs. Ces sondages ne sont
ni un inventaire de toute l'île, ni un benchmark. Les essais simultanés ne
permettent pas de conclure à un gain de performances. Aucun test Windows ni
validation esthétique par le joueur n'est revendiqué.
Le complexe est un premier plan allongé fixe, dont le site et l'orientation
s'adaptent à la graine. Les surplombs sont voulus ; les piliers s'arrêtent au bout
de 24 blocs s'ils ne rencontrent pas de roche. Une recherche bornée à 96 positions
préfère une attache au terrain sans exiger une emprise parfaitement pleine.
L'accessibilité extérieure sur toutes les graines reste à apprécier en jeu.
MRpack normal exporté localement, ZIP intact, JAR exact vérifié, trois tailles
211/212 présentes, aucun monde ni module Test embarqué. Minecraft 26.3,
Fabric Loader 0.19.5. Copie `~/Downloads/Sanctuary-beta.212.mrpack`.
SHA-256 : `5ba08e99fd9c656df2199f8f0f479ce89cfd442d2e8f5ef00376294b8c5f02be`.
Reçu `build/south212-mrpack-receipt.json`. Aucun canal public ou tag publié.
Visite préparée `Sanctuary-South-212-42`, depuis l'essai final arrêté, sous
`build/worldgen-lab/south212-visit2/small/42/client/saves/`. Départ en vol
**(8.5, 326, 841.5)** face au temple **(64, 250, 912)** ; créatif, commandes,
32 chunks, simulation 5. Arrivée **(120, 249, 680)** ; relais **(128, 251, 1224)**.
Client ouvert à **06:10:21 CEST le 5 octobre 2026** : `SOUTH212_VISIT_OPEN`,
position et monde attendus. Backend Vulkan et distance 32 confirmés dans les
traces / options. Le rendu reste à apprécier par le joueur.
+113
View File
@@ -0,0 +1,113 @@
# WG-211 — Sud fracturé, badlands extérieurs et intérieur coloré
Branche `codex/south-shattered-beta211`, depuis beta.210 (`2ee53e1`).
## Contrat
R012 rejette le rendu et la forme 210 : trop uniforme, gradins visibles et
cavités tronquées. Repartir du canyon historique et du bruit 3D, sans Voronoi,
avec des badlands chauds dehors et des couleurs dedans. Diamètre confirmé à
1024 blocs. Hauteur actuelle comme référence ; dessous d'épaisseur variable.
Huit teintes internes pour ce prototype, nombre encore ouvert au jugement visuel.
Nouveaux profils 211 uniquement. Les sauvegardes et profils antérieurs restent
inchangés. Contrat de test : nouvelles graines 42/0, activation réelle du Sud,
interruption/reprise, inspection native et nouvelle visite Vulkan à 32 chunks.
Export MRpack local ; aucun canal public, serveur personnel ou Prism modifié.
## Références lues
Code Minecraft 26.3 local : `MaterialSystem.generateBands`, `getBand`,
`erodedBadlandsExtension` dans le JAR officiel utilisé par Loom. Les bandes
natives seront appelées directement. Le relief flottant réutilise le bruit 3D
natif déjà employé par les anciennes expansions, à son échelle en blocs ;
il ne remplace pas le monde entier par le générateur Overworld.
La [présentation officielle des Badlands](https://www.minecraft.net/en-us/article/around-block--badlands)
sert de référence d'ambiance ; la compatibilité est vérifiée sur le code 26.3.
Implémentation compilée et contrôles natifs réussis ; rendu à juger en visite.
## Réalisation
- `SouthNoise211` compile le champ flottant natif via `NorthNoise205`, sans
les montagnes Voronoi, gradins 209, remappage des plateaux ni canyon axial
d'`ExpansionRelief`. Des fissures en bruit 3D déformé traversent plusieurs
secteurs ; leurs terminaisons varient aussi en hauteur. La transformation
retire de la matière, sans reboucher les vides du champ initial.
- Le dessous suit le toit natif avec une épaisseur théorique de 44–102 blocs,
modulée continûment puis érodée. Les cavités et surplombs du bruit restent.
Caches bornés par worker ; pas de simulation d'hydrologie ni de carte globale.
- L'extérieur appelle `MaterialSystem.getBand` du Minecraft installé via un
invoker : vraies bandes natives et leur décalage, déterministes par graine.
L'intérieur emploie huit terracottas (rouge, orange, jaune, vert clair, cyan,
bleu, violet, magenta), en couches inégales légèrement plissées. Leur volume
reste sous le toit, au-dessus du dessous et à l'intérieur de la roche native.
Les fissures en révèlent des faces ; les peaux extérieures restent chaudes.
- Sable rouge soutenu, plages de terre stérile, herbes sèches, buissons morts et
cactus sur les surfaces peu inclinées. Aucun ajout de structure à ce lot.
- Les trois tailles de Sanctuary utilisent de nouveaux presets 211. Les anciens
presets, palettes et sauvegardes sont conservés. L'île de départ conserve ses
trois densités identiques ; le Nord continue d'utiliser 208. Expansion Sud 1024.
## Contrôles de code
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south210,south211 -PsanctuaryAtlasOnly=true`
exécute les contrôles purs, puis 9 des 10 GameTests passent. Le test des fractures
échantillonnait uniquement au-dessus de Y230, excluant les sommets plus bas d'un
secteur. Le relevé a été corrigé pour examiner les 30 blocs sous chaque toit,
sans modifier le générateur. Traces `build/south211-check-build.log` et
`build/south211-shape-check.log` conservées.
Reconstruction ciblée avec `:sanctuary:runGameTest build assemblePack
assembleTestPack :sanctuary-test:exportDuoLaunch -x :sanctuary:check`, même sélection :
succès en 20 s, **10 GameTests passés** (`build/south211-final-build.log`).
Couverture : profils publics/historiques, Nord accepté, ancienne palette 210,
soustraction seule du nouveau relief, fissures réparties, ordre des workers,
compilation native, huit teintes internes et bandes natives effectivement appelées.
La suite historique complète des GameTests n'est pas revendiquée.
## Essais natifs et distribution
Nouveaux mondes `south211-final` (42) et `south211-final0` (0), profil Small pour
l'île de départ et expansion Sud 1024. Ancre réelle, refus du mauvais paiement
et du doublon, interruption 0/58, reprise 58 chunks FULL, appuis du relais et
signal SGA vérifiés. Les deux serveurs sont arrêtés proprement après leurs tests.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Hauteurs de la grille intérieure | 82–254 | 80–254 |
| Colonnes rocheuses | 1563 | 1462 |
| Ouvertures sondées sous le toit | 583 | 728 |
| Colonnes avec biome attendu | 300 | 300 |
| Blocs natifs inspectés | 10350 | 9935 |
| Blocs de cœur aux teintes froides | 1550 | 2236 |
| Dont faces adjacentes à de l’air | 29 | 70 |
| Blocs contrôlés dans les peaux chaudes | 3688 | 3460 |
| Sables / sans support | 137 / 0 | 74 / 0 |
Les huit couleurs internes sont contrôlées dans les GameTests. Les cinq teintes
froides distinctes de la peau vanilla sont toutes observées dans les deux
échantillons natifs. Plantes sèches présentes et soutenues. Ces comptages ne
couvrent pas l'île entière et ne valident ni l'esthétique ni les performances.
Les essais simultanés ne sont pas un benchmark ; aucun test Windows n'est revendiqué.
`build/Sanctuary-beta.211.mrpack`, copié dans `~/Downloads/`. Archive ZIP intacte,
version et JAR exact vérifiés, trois tailles 210/211 présentes, aucun monde ni
module Test embarqué. Minecraft 26.3, Fabric Loader 0.19.5.
SHA-256 :
`1779f71893b3feb3d72f64e9a262471a58df676b715ec6831213833bd697f96c`.
Reçu `build/south211-mrpack-receipt.json`. Pas de publication de canal ou de tag,
pas de déploiement Prism. Les anciennes visites sont conservées.
Nouvelle visite `Sanctuary-South-211-42`, copie de l'essai arrêté, sous
`build/worldgen-lab/south211-visit/small/42/client/saves/`. Vulkan, 32 chunks,
simulation 5, créatif en vol, commandes. Départ **(136.5, 288, 889.5)**, regard
vers **(192, 264, 960)**. Arrivée **(128, 249, 664)** ; relais **(128, 251, 1224)**.
Une paroi colorée échantillonnée se trouve autour de **(120, 186, 656)**.
Client ouvert à 05:27:18 CEST le 5 octobre 2026 : `SOUTH211_VISIT_OPEN`,
monde et position attendus, backend Vulkan confirmé. Les temples et minerais propres
au Sud restent pour la suite.
+111
View File
@@ -0,0 +1,111 @@
# WG-214 — Village abandonné du canyon
Branche `codex/south-western-beta214`, depuis beta.213 (`da838b9`).
## Contrat
Ajouter à la surface du Sud un village Far West décoratif en chêne noir :
sept ou huit maisons, rue principale, trois façades d’un côté et quatre ou
cinq de l’autre, porches, ornementation et lanternes. Disposition, dimensions
et usure varient avec la graine. Pas de PNJ, quête ou nouveau butin.
Relief, palette, pyramide et relais 213 conservés.
Nouveaux profils 214 uniquement. Aucun ancien monde, chunk ou journal converti.
Les profils historiques gardent leur recette. Vérification sur des mondes
jetables neufs, activation réelle de l’ancre Sud puis reprise autorisées pour
ces essais ; aucune activation dans une partie existante. Visite neuve Vulkan
32 chunks, distribution locale ; aucun canal public ou Prism modifié.
## Réalisation
`SouthVillage214` ajoute un village par expansion Sud 214, séparé du temple,
de l’arrivée et du relais. Sept ou huit maisons : trois d’un côté, quatre ou
cinq en face, avec inversion possible des côtés. Rue de 83 blocs, largeur
neuf ; trois rotations supplémentaires possibles. Façades de 7 à 11 blocs,
profondeur de 9 à 11, porches, corniches, fausses façades à décrochements,
toits inclinés, vitres brisées ou condamnées, toiles et mobilier simple.
Chêne noir, lanternes suspendues espacées, sol en terre stérile / enracinée.
Aucun coffre, table de butin, piège, villageois ou objectif ajouté au village.
Recherche déterministe et bornée (grille de 32 blocs) sur les surfaces déjà
produites. Rue suivant
les hauteurs réelles, raccords limités à trois blocs de coupe/remblai dans
la rue, fondations d’au plus cinq blocs vérifiées colonne par colonne, au
plus un bloc de roche dépassant le plancher des maisons. Le plan ne charge
aucun chunk et se conserve en cache ; pose indexée par chunk, réservée avant
la végétation. La préparation de l’expansion comprend les chunks du village.
Un site non supporté est refusé : une graine défavorable peut ne pas recevoir
de village, plutôt que produire des fondations traversant le vide.
Les profils 214 réemploient le terrain, la palette, le biome et la pyramide
213. Pas de copie divergente du générateur. Les profils antérieurs conservent
leur génération ; Nord et île principale inchangés.
## Vérifications
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,north208,south213,south214 -PsanctuaryAtlasOnly=true`
réussi en 2 min 50 s (`build/south214-verified-build.log`). Contrôles purs et
**9 GameTests ciblés** réussis. La suite historique complète des GameTests
n’est pas revendiquée.
Le nouveau test vérifie sur deux graines directes et la graine dérivée de
l’expansion du monde 0 les maisons accessibles depuis la
rue, les supports du plan, les lampes, la limite de chunks et l’absence de
conteneurs / TNT. Les tests 213 conservent les contrôles de la pyramide et du
relief ; le profil public courant et toutes les anciennes clés 196–213 sont
résolus sans module Test. Les trois fichiers de bruit de l’île principale
214 sont identiques octet pour octet aux fichiers 213.
Pendant le développement : deux usages d’API ChunkPos ont été adaptés à 26.3 ;
un test de sélection attendait encore 213 et a été actualisé en conservant
l’assertion historique. Relecture des marches : leur hauteur rejoint désormais
la rue aménagée, pas le sol latéral avant aménagement. La première génération native a trouvé le village sur 42, mais pas sur 0.
La recherche affinée et la rue légèrement nivelée corrigent ce cas ; cette
graine dérivée, avec ses coordonnées réelles, rejoint le test de placement.
La dernière commande ci-dessus vérifie cette version finale.
## Essais natifs
Nouveaux mondes `south214-verified` (42) et `south214-verified0` (0), île Small,
expansion Sud 1024. Activation réelle de l’ancre, mauvais paiement / doublon
refusés, interruption puis reprise et relais final contrôlés. Les serveurs
sont arrêtés proprement après chaque phase. Aucune ancienne visite modifiée.
| Relevé | Graine 42 | Graine 0 |
| --- | --- | --- |
| Centre du village | −64, 251, 880 | 32, 251, 976 |
| Maisons | 8 (3 + 5) | 8 (3 + 5) |
| Chunks du village | 22 | 23 |
| Lanternes / toiles | 6 / 10 | 6 / 10 |
| Chunks préparés pour l’expansion | 89 | 87 |
| Coffres / TNT de la pyramide | 10 / 24 | 10 / 24 |
Tous les blocs du plan sont comparés aux blocs des chunks réels. Supports
sous les planchers, hauteur des portes, parcours rue → intérieur et tenue des
lanternes vérifiés. La pyramide conserve ses dix piles enchantées ; le contrôle
des matériaux ne trouve ni les anciens acacias ni les sables supprimés en 213.
Les caches de plan sont reconstruits à la réouverture aux mêmes emplacements.
Ces relevés ne constituent pas un benchmark, un test Windows ou une validation
esthétique. Sur les deux graines directes du test de placement, les plans ont
respectivement huit et sept bâtiments ; les deux visites natives en ont huit.
## Visite
Nouveau solo `Sanctuary-South-214-42`, dossier
`build/worldgen-lab/south214-visit/small/42/client/saves/`.
Créatif en vol, commandes, ciel clair, 32 chunks / simulation 5, Vulkan.
Départ **(−12.5, 265, 880.5)**, face à la rue centrée en **(−64, 251, 880)**.
Ouvert le 5 octobre 2026 à **07:21:04 CEST**, confirmé par
`SOUTH214_VISIT_OPEN` avec le monde et la position attendus. Backend Vulkan
(MoltenVK 1.4.2) et options 32/5 confirmés dans les logs / réglages. Le jugement
visuel appartient à la nouvelle visite ; aucune capture ou approbation du joueur
n’est annoncée.
## Distribution locale
MRpack exporté, intégrité ZIP, version et JAR exact contrôlés ; profils 213/214
Small/Medium/Large présents, aucun monde ou module Test inclus.
`~/Downloads/Sanctuary-beta.214.mrpack`.
SHA-256 : `4e28fc3c5f00464f2fb37f7b2bcbba9ef8edcd23fffdc8a321d26f2bd31ddb02`.
Reçu : `build/south214-mrpack-receipt.json`. Aucun canal public ou tag publié.
+82
View File
@@ -0,0 +1,82 @@
# WG-219 — Plages basses et palmiers de l’Ouest
Branche `codex/west-beaches-beta219`, depuis beta.218 (`3adb08e`).
## Contrat
L’océan 218 est validé : conserver exactement le fond, la fracture, le temple,
les coraux et les épaves à graine identique. Abaisser les berges à la hauteur
de l’eau sur trois à quatre blocs, puis monter doucement. Concentrer beaucoup
de palmiers sur cette plage intérieure, retirer ceux du bord extérieur.
Retirer soufre, cinabre, pointes, sources et biome sulfureux de la couronne Ouest.
Ne pas retirer le soufre des autres régions de Sanctuary.
Nouveaux profils 219 Small/Medium/Large ; historiques conservés, aucun monde
ancien modifié ou régénéré. Contrat d’essai : mondes jetables neufs 42/0,
activation de l’ancre Ouest, interruption/reprise. MRpack local et nouvelle
visite Vulkan 32 chunks. Aucun push, tag, canal public ou mise à jour Prism.
## État
Implémentation et vérifications terminées. MRpack local vérifié ; rendu des
nouvelles berges à apprécier en jeu.
## Mise en œuvre
Le champ océanique 218 est partagé : aucune modification sous l’eau, mêmes
biomes de récif et de fosse. Les sites des quatre épaves sont repris du calcul
historique avec sa séquence aléatoire, indépendamment des nouveaux palmiers.
Le plan temporaire est libéré ; seuls les quatre emplacements sont conservés.
Sur la terre sèche, une bande de sable reste à Y=220 sur environ quatre blocs,
au lieu de démarrer à Y=221. Première marche ensuite, puis une montée étalée
vers de petites hauteurs. Les fondations de l’arrivée et du relais gardent
leurs positions. La cuve et son dessous restent fermés.
Les palmiers suivent le contour intérieur à 0,7–4,2 blocs du rivage calculé,
sur le sable à Y=220, avec variations de hauteur, inclinaison et espacement.
Pas de palmiers sur la couronne extérieure. La terre intérieure conserve une
végétation basse. Soufre, cinabre, pointes, sources et biome sulfureux retirés
uniquement de l’Ouest 219 ; les anciens mondes et les autres régions conservent
leurs matériaux.
## Vérifications du 5 octobre 2026
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,west218,west219 -PsanctuaryAtlasOnly=true` :
réussi, six GameTests obligatoires passés. Contrôles de terrain sur six graines :
fond, matériaux immergés et biomes marins identiques à 218 ; plage basse,
première montée, palmiers et absence de matériaux sulfureux vérifiés.
Les profils historiques restent disponibles, dont les trois tailles 218.
Essais natifs sur deux nouveaux mondes Small, avec activation Ouest,
interruption à 0/94 chunks puis reprise jusqu’à READY :
| Graine du monde | Centre Ouest | Graine de l’expansion | Palmiers planifiés | Colonnes de plage vérifiées | Épaves / coffres |
| --- | --- | --- | --- | --- | --- |
| 42 | −960, 0 | −8263140775942472950 | 431 | 1510 | 4 / 12 |
| 0 | −944, 128 | −189784416336457674 | 431 | 1256 | 4 / 12 |
Huit troncs contrôlés physiquement sur chaque monde, zéro geyser. Monument,
relais et falaises vérifiés ; 94 chunks préparés dans chaque cas. Les essais
natifs portent sur Small ; Medium/Large sont assemblés avec les mêmes règles
d’expansion, sans visite native supplémentaire sur cette livraison.
MRpack : `build/Sanctuary-beta.219.mrpack`, également copié dans Downloads.
Archive intègre, JAR strictement identique au build, version 219,
profils historiques et trois tailles présents ; aucun monde ni module de test.
SHA-256 : `9be5755c0f53f33988c07cf8e979c02bd6bb137bf4565502a24545ab19201042`.
JAR SHA-256 : `4265e78a203a3bd950771233c337e125c0aaed7080da526d1b8a749c4c25dc02`.
Traces locales : `build/west219-build-ready.log`,
`build/worldgen-lab/west219-final/small/42/{cold,warm}.json`,
`build/worldgen-lab/west219-final0/small/0/{cold,warm}.json` et
`build/west219-mrpack-receipt.json`. Aucun monde existant migré ou régénéré.
Visite neuve `Sanctuary-West-219-42` ouverte à 15:16:09 (Europe/Paris),
Vulkan/MoltenVK, vue 32 chunks, simulation 5, créatif et commandes activés.
L’ancienne visite 218 a été arrêtée proprement et sa sauvegarde conservée.
La nouvelle visite est une copie du monde de vérification 42 arrêté ;
`WEST219_VISIT_OPEN` confirmé dans le journal. Aucun essai graphique OpenGL.
Les mesures vérifient le placement ; l’appréciation visuelle des nouvelles
berges et de la densité des palmiers reste au joueur.
+86
View File
@@ -0,0 +1,86 @@
# WG-220 — Couronne vivante et plages libres de l’Ouest
Branche `codex/west-living-rim-beta220`, depuis beta.219 (`988a6c8`).
## Contrat
Conserver l’océan validé : eau, fond marin, fracture, monument et épaves.
Élargir la plage basse à 12–15 blocs libres ; déplacer les palmiers en retrait.
Rendre la couronne extérieure irrégulière par du relief et des creusements 3D,
avec ravins et surplombs. Garder une paroi pleine entre ces creusements et l’eau.
Sur l’herbe : arbres, buissons, fleurs. Sur le sable : cactus, bois mort,
buissons secs et herbes jaunes. Soufre et cinabre restent absents de cet Ouest.
Nouveaux profils 220 Small/Medium/Large, historiques conservés. Aucun monde
ancien modifié ou régénéré. Contrat d’essai : mondes jetables neufs 42/0,
activation de l’ancre Ouest et interruption/reprise. MRpack local et visite
Vulkan 32 chunks. Aucun push, tag, canal public ou déploiement Prism.
## État
Implémentation et vérifications terminées ; rendu du pourtour à apprécier en jeu.
## Mise en œuvre
Le champ marin 218/219 est repris sans changement pour toutes les colonnes
immergées. Seules les colonnes terrestres reçoivent le nouveau champ 220 :
largeur de plage variable de 12 à 15 blocs, puis reliefs, creux et ravins.
La limite extérieure et les surplombs varient en trois dimensions ; des
cavités s’ouvrent dans la couronne. Les 22 premiers blocs depuis la rive
gardent une paroi pleine sous le niveau marin. Arrivée et relais conservent
leurs fondations et positions.
Le sol utilisable est recherché dans la densité finale, puis mis en cache
par colonne. Les sables et sols meubles ne coiffent que des surfaces soutenues.
Les palmiers commencent en retrait à partir de 28 blocs de la rive ; aucun
bloc de végétation n’empiète sur les 17 premiers blocs. Les bosquets combinent
palmiers, chênes et buissons, fleurs par taches et herbes. Les zones sableuses
reçoivent cactus, arbres morts en chêne écorcé, buissons et herbes sèches.
Chaque décor est admis entièrement dans l’air du terrain final avant sa pose
par chunk ; les emplacements sont déterministes et calculés une fois.
## Vérifications du 5 octobre 2026
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,west219,west220 -PsanctuaryAtlasOnly=true` :
réussi ; cinq GameTests obligatoires passés. Le contrôle 220 couvre six graines :
0, 42, −6763683964522674921, 3285501279717306940,
−8263140775942472950 et −189784416336457674. Comparaison des états immergés
avec 219, plage libre, paroi pleine, creux et hauteurs, cavités, sable soutenu,
végétation variée en retrait, déterminisme et positions des structures.
Les trois réglages de bruit de l’île principale sont identiques à 219 ;
les anciens profils sont conservés.
MRpack intègre : JAR identique au build, profils historiques et trois tailles
présents, aucun monde ni module de test. Copie dans Downloads.
SHA-256 MRpack : `a7f176e18041ac5bcc764ca0461e22551e08743af7475fc53bc156c18f8299d2`.
SHA-256 JAR : `cf1352ed4d7d1c1a9f8582986b64befef1be91184fe7dc4259dff9f46f005c66`.
Traces locales : `build/west220-build-ready.log` et
`build/west220-mrpack-receipt.json`.
Essais natifs Small sur les graines 42 et 0 : activation de l’ancre Ouest,
interruption à 0/94 chunks et reprise réussies. Expansion de 1024 blocs,
94 chunks de préparation dans chaque cas. Aucune migration d’ancien monde.
| Graine | Centre Ouest | Graine de l’expansion | Palmiers en retrait | Colonnes de plage vérifiées |
| --- | --- | --- | --- | --- |
| 42 | −960, 0 | −8263140775942472950 | 235 | 3207 |
| 0 | −944, 128 | −189784416336457674 | 246 | 2688 |
Sur chaque monde : huit troncs de palmier et 21 décors de sept catégories
contrôlés physiquement, survie native des plantes vérifiée. Quatre épaves et
douze coffres de butin, monument complet et relais fonctionnel conservés.
Falaises immergées et eau contrôlées. Les essais de volume comparent le terrain
marin ; ils ne garantissent pas un placement identique de chaque corail natif.
La nouvelle végétation et la silhouette extérieure restent à apprécier en jeu.
Medium/Large sont assemblés avec les mêmes règles d’expansion, sans nouvelle
visite native de ces tailles pour cette livraison.
Traces : `build/worldgen-lab/west220-final/small/42/{cold,warm}.json` et
`build/worldgen-lab/west220-final0/small/0/{cold,warm}.json`.
Nouvelle visite `Sanctuary-West-220-42` ouverte le 5 octobre à 16:08:19
(Europe/Paris), Vulkan/MoltenVK, 32 chunks, simulation 5, créatif avec vol et
commandes. Copie du monde de vérification 42 arrêté ; anciennes sauvegardes
conservées. Confirmation `WEST220_VISIT_OPEN` dans le journal client.
Pas de validation visuelle anticipée ni de test OpenGL.
+118
View File
@@ -0,0 +1,118 @@
# WG-218 — Plateau sous-marin et oasis de l’Ouest
Branche `codex/west-oasis-beta218`, depuis beta.217 (`f8456df`).
## Contrat
Océan Ouest de 1024 blocs : plateau sous-marin, falaises irrégulières vers
une fracture centrale et monument océanique obligatoire au centre.
Suppression des racines ; couronne de plages de sable et grès, végétation,
palmiers, biomes de soufre avec cinabre et épaves natives. Aucune calcite
sur ce nouvel Ouest. Conserver l’île principale, le Nord et le Sud.
Nouveaux profils 218 Small/Medium/Large uniquement ; profils historiques
conservés. Aucun monde existant modifié ou régénéré. Contrat d’essai : mondes
jetables neufs 42 et 0, activation de l’ancre Ouest, interruption et reprise.
Préparation limitée aux destinations utiles ; aucune hydrologie globale.
Livraison locale MRpack et visite Vulkan 32 chunks. Aucun push, tag, canal
public ou installation Prism à modifier.
## État
Implémenté ; compilation, sept GameTests et essais natifs 42/0 réussis.
MRpack local vérifié. Le rendu reste à apprécier dans la nouvelle visite.
## Mise en œuvre
Eau à Y=220 ; plateau sableux généralement vers Y=198–206, rupture étroite
vers une fosse allongée, sinueuse et orientée par la graine. Fondation centrale
du monument à Y=136. Le dessous de la cuve reste sous le point le plus profond,
indépendamment des changements de hauteur du fond : une falaise ne peut plus
passer sous le dessous de sa voisine. Bruit 3D sur le dessous et le bord externe,
avec une enveloppe protégée autour de l’eau. Aucune racine ajoutée.
Couronne de grès et plages sableuses, herbe et palmiers à troncs courbés,
frondes de jungle persistantes. Trois grands secteurs irréguliers de soufre,
cinabre en veines, pointes natives et petites sources de soufre actif. Cinq
biomes versionnés isolent cette écologie des autres expansions. Pas de calcite
ni de terracotta dans le nouveau terrain ; grès aussi sur les finitions du relais.
Quatre épaves natives variées sont placées sur les hauts-fonds autour de la
fosse, avec les tables de butin natives. Leur altitude est fixée par le champ
avant décoration, pour ne pas dépendre du premier chunk chargé. Plans de
végétation et structures déterministes, placement découpé par chunk. Le temple
conserve son graphe de salles et ses trois gardiens anciens ; pas de nouveau
StructureStart, donc la limitation de localisation et de réapparition native
des gardiens ordinaires de 217 reste applicable.
La préparation avant READY reste bornée à l’arrivée, au temple et au relais.
Les épaves et la végétation apparaissent à l’exploration. Aucun scan hydrologique
mondial ou calcul de trajet de rivière.
## Vérifications
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,west217,west218 -PsanctuaryAtlasOnly=true` réussit :
contrôles purs et sept GameTests. Le premier passage avait un mauvais nom de
climat dans un test de conservation du Nord ; corrigé en `glacial`.
Le contrôle géométrique couvre six graines, les fondations du monument, les
parois sous-marines sur toute leur hauteur, les plages et les secteurs
sulfureux, la reproductibilité des détails et l’isolement des profils 217/218.
Les trois fichiers de bruit de l’île principale sont identiques à ceux de 217.
Deux ajustements ont concerné uniquement le diagnostic natif, sans changer le
JAR distribué : huit chunks côtiers trop peu profonds ne suffisaient pas à
relever les coraux de 42 (présents dans sept chunks voisins sauvegardés) ;
le relevé utilise seize points plus profonds. Une recherche de falaise s’arrêtait
à 70 blocs du centre, trop tôt pour un bras étroit de la fracture de 42 ; elle
va désormais jusqu’au centre. Chaque nouvel essai utilise un dossier neuf.
Essais terminés : `west218-delivery` (42) et `west218-final0` (0), avec le même
JAR Sanctuary issu du build réussi. La dernière extension de recherche du
contrôle des falaises ne modifie que le module Test. Activation par l’ancre
Ouest, paiement exact, refus du doublon, interruption à 0/94 et reprise READY.
| Relevé | Graine 42 | Graine 0 |
| --- | ---: | ---: |
| Graine dérivée | -8263140775942472950 | -189784416336457674 |
| Chunks préparés | 94 | 94 |
| Palmiers planifiés | 294 | 307 |
| Pointes de soufre | 198 | 207 |
| Sources géothermales | 17 | 18 |
| Cinabre dans les échantillons | 3176 | 2660 |
| Épaves / coffres de butin | 4 / 12 | 4 / 12 |
| Coraux dans seize chunks | 24 | 124 |
| Or du temple | 8 | 8 |
| Gardiens anciens sauvegardés | 3 | 3 |
| Blocs du relais comparés au plan | 2069 | 2069 |
Temple 42 : `(−960, 137, 0)` ; relais `(−1355, 238, 0)`.
Temple 0 : `(−944, 137, 128)` ; relais `(−1341, 238, 128)`.
Les trois gardiens sont relus dans les fichiers d’entités après arrêt.
Contrôle complémentaire des chunks sauvegardés entre Y=112 et Y=220 :
727 chunks / 433 820 interfaces eau/autre sur 42 ; 703 / 431 123 sur 0.
Aucune ouverture d’une colonne d’eau vers le vide inférieur dans les données
contrôlées. Quelques poches d’air fermées subsistent près des structures et
de la décoration native (55 cellules sur 42, groupes de cinq au maximum ;
21 sur 0, groupes de trois au maximum). Le volume intérieur du monument et
les chunks absents ou incomplets sont exclus ; ce contrôle ne représente pas
une prégénération de toute l’expansion. Reçus ignorés : `build/west218-water42.json`
et `build/west218-water0.json`. Aucun benchmark de fluidité ou essai Windows.
## Livraison locale
MRpack normal Small/Medium/Large, sans module Test ni sauvegarde,
copié dans `/Users/koka/Downloads/Sanctuary-beta.218.mrpack`.
Intégrité ZIP, version, profils historiques et identité exacte du JAR vérifiées.
SHA-256 MRpack : `feecc10ca1f5873bc795a683f5d31cb5b7ad5d6484e4880cdec0841318b4d9ee`.
SHA-256 JAR : `feddc40854c3a1c8b321426f1007e967498b11c287dc28604ed0b3bfda03318a`.
Visite neuve `west218-visit`, monde `Sanctuary-West-218-42`, copie du monde
vérifié et arrêté. Créatif, commandes, vol, vue 32 chunks et simulation 5 ;
départ `(−564, 269, 0)` vers le centre de la mer. Aucun ancien monde modifié.
Ouverture confirmée le 5 octobre 2026 à 14:51:30 CEST par
`WEST218_VISIT_OPEN` ; backend Vulkan/MoltenVK et options 32/5 confirmés.
Aucun push, tag, canal packwiz public ou instance Prism modifié.
+114
View File
@@ -0,0 +1,114 @@
# WG-217 — Mer suspendue de l’Ouest
Branche `codex/west-ocean-beta217`, depuis beta.216 (`0eb4517`).
## Contrat
Prototype jouable : mer de diamètre nominal 1024, cuve rocheuse étanche,
ramifications minérales depuis l’intérieur vers le vide et déformation 3D.
Bords clairs et ocres inspirés de Yellowstone, hauts-fonds et coraux.
Un monument océanique natif obligatoire, avec fondation et profondeur réservées
dans le calcul du terrain. Relais régional visible sur la rive extérieure.
Nouveaux profils 217 Small/Medium/Large uniquement ; anciens profils, île
principale, Nord et Sud conservés. Aucun monde existant modifié ou régénéré.
Contrat d’essai : mondes jetables neufs sur les graines 42 et 0, activation
par l’ancre Ouest, interruption et reprise autorisées. Préparation limitée aux
lieux utiles, sans hydrologie globale ni génération préalable de toute la mer.
MRpack local et nouvelle visite solo Vulkan à 32 chunks. Aucun push, canal
public ni mise à jour de Prism.
## État
Implémenté, contrôles réussis et MRpack vérifié. Nouvelle visite Vulkan ouverte ;
le rendu reste à apprécier en jeu.
## Mise en œuvre
La mer utilise un champ géographique déterministe commun au terrain, aux
biomes et au placement du temple. Eau à Y=220, fond variable et hauts-fonds
sur les racines sous-marines ; 9 à 12 départs de racines bifurquent vers le
vide. Deux bruits 3D irrégularisent les volumes extérieurs et un troisième
ouvre des cavités au-delà du collier étanche. Les rives mêlent calcite,
grès, touches de terracotta et roche. Pas de simulation hydrologique globale :
les sources d’eau sont posées localement pendant la génération des chunks.
Le temple reprend le graphe de salles du monument océanique Minecraft 26.3,
son trésor, ses éponges éventuelles et ses gardiens anciens. Le champ réserve
une fondation à Y=153 et une profondeur suffisante, quelle que soit la graine.
La pièce extérieure **et** ses salles internes sont translatées ensemble.
Placement déterministe, découpé par chunk, comme le manoir régional existant.
Cette première implantation ne crée pas de StructureStart natif : pas de
localisation vanilla ni de zone native de réapparition des gardiens ordinaires.
Trois biomes isolés reprennent l’ambiance et la végétation sous-marine natives,
sans carvers, minerais génériques ou sources d’eau susceptibles de percer la
cuve. Le monument et le relais sont préparés avant l’annonce SGA ; le reste
de la mer se génère à l’approche. Le relais reprend le socle régional en
prismarine/calcite et pointe vers l’Ouest. Les générations ultérieures de ce
relais utilisent encore la même famille océanique dans ce prototype ; la
variété des expéditions lointaines reste un chantier séparé.
## Vérifications et essais
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,west217 -PsanctuaryAtlasOnly=true` réussit :
contrôles purs et cinq GameTests. La passe précédente a également validé les
contrôles ciblés des relais 215 et du Sud 216. Les trois fichiers de bruit 217
sont identiques aux 216 ; les préréglages n’ajoutent que la nouvelle révision
et les trois biomes de l’Ouest. Libellés FR/EN présents.
Le premier contrôle a trouvé que l’aplanissement de l’arrivée empiétait sur
la mer : cette modification du sol est désormais limitée à la rive sèche.
Le premier relevé natif cherchait huit blocs d’eau sur chaque rayon ; les
racines donnent parfois des récifs à cinq blocs de profondeur. Le relevé
côtier a été corrigé pour inclure ces hauts-fonds, sans changer le terrain.
Seul le module Test a été recompilé après le build final ; le JAR distribué
reste exactement celui du `check build` réussi.
Essais définitifs : `west217-verified` sur 42 et `west217-verified0` sur 0.
Activation par l’ancre Ouest, paiement exact, refus du doublon, interruption
à 0/83 puis reprise jusqu’à READY. Pas d’activation sur une sauvegarde antérieure.
| Relevé natif | Graine 42 | Graine 0 |
| --- | ---: | ---: |
| Centre X/Z | −960 / 0 | −944 / 128 |
| Graine dérivée | −8263140775942472950 | −189784416336457674 |
| Chunks préparés avant READY | 83 | 83 |
| Segments de racines | 33 | 27 |
| Colonnes côtières contrôlées en jeu | 2 048 | 2 048 |
| Coraux dans ces huit chunks | 5 | 20 |
| Blocs de prismarine du temple | 13 096 | 13 135 |
| Lanternes marines | 124 | 118 |
| Blocs d’or | 8 | 8 |
| Gardiens anciens dans les fichiers sauvegardés | 3 | 3 |
| Blocs du relais comparés au plan | 2 069 | 2 069 |
Temple 42 : `(−954, 154, −29)`, relais `(−1351, 244, 0)`.
Temple 0 : `(−980, 154, 138)`, relais `(−1343, 244, 128)`.
Les trois gardiens de chaque temple sont relus dans les fichiers d’entités
après arrêt du serveur. Les rapports couvrent la préparation utile et des
échantillons du littoral ; ils ne constituent ni une prégénération complète,
ni un benchmark de performances, ni un essai Windows, ni une validation visuelle.
## Livraison locale
MRpack normal avec Small, Medium et Large, aucun module Test ni monde :
`build/Sanctuary-beta.217.mrpack`, copié dans Downloads. Intégrité ZIP,
version, profils historiques et identité exacte du JAR vérifiées.
SHA-256 MRpack : `1244b296efb4c10da2286d5764cf311201f23a73b72c046b997255532acad386`.
SHA-256 JAR : `d39bdc865a23bf605197b5672dfe5a5cff9cfbc7c9b4ad3a49125e331b86b6b5`.
Visite neuve `west217-visit-final`, monde `Sanctuary-West-217-42`, copie du
monde vérifié, créatif avec commandes et vol. Vue 32 chunks, simulation 5,
départ `(−576, 275, 0)` face à la mer. Ouverture confirmée le 5 octobre 2026
à 14:11:06 CEST par `WEST217_VISIT_OPEN` ; backend Vulkan/MoltenVK confirmé.
Aucun canal public, tag ou installation Prism mis à jour.
Lecture complémentaire des chunks sauvegardés à Y=220 : 55 808 colonnes et
1 142 contacts eau/rive contrôlés sur 42 ; 55 040 colonnes et 1 371 contacts
sur 0. Aucun contact latéral eau/air trouvé dans les chunks dont le terrain
est disponible. Les chunks absents ou incomplets sont exclus, sans extrapoler
ce relevé à toute l’expansion. Reçus ignorés : `build/west217-water42.json`
et `build/west217-water0.json`.
+95
View File
@@ -0,0 +1,95 @@
# WG-221 — Pierre jaune et parois de l’Ouest
Branche `codex/west-yellow-stone-beta221`, depuis beta.220 (`58aa4c2`).
## Contrat
Retirer les arbres morts en bois écorcé et la passe de ravins de 220.
Conserver le modelé 3D, avec un Voronoi discret pour former des parois plus
droites. Agrandir les zones de sol cohérentes et réserver sable/herbe aux
surfaces douces ; nouvelle pierre jaune pour les masses rocheuses.
Nouveau bloc `sanctuary:yellow_stone`, mineable, ramassable et constructible,
libellés FR/EN, juste après Stone dans les onglets Naturel et Construction.
Première apparence : grain de la stone native et teinte jaune
grès appliquée par le système de couleurs natif, sans nouvelle texture lourde.
Océan, monument, épaves, plage libre de 12–15 blocs et végétation vivante conservés.
Nouveaux profils 221 Small/Medium/Large uniquement ; anciens profils et mondes
conservés. Contrat d’essai : mondes jetables neufs 42/0, activation Ouest et
interruption/reprise. MRpack local et visite Vulkan 32 chunks. Aucun push,
tag, canal public ou déploiement Prism.
## État
Implémentation et vérifications terminées ; nouveau rendu à apprécier en jeu.
## Mise en œuvre
Le profil 221 enlève la soustraction de 39 blocs qui formait les ravins de 220.
Le volume 3D, les cavités et les surplombs sont conservés. Des cellules Voronoi
de 76 blocs apportent 35 % du relief terrestre ; les raccords sont adoucis sur
quatre blocs et le bruit de surface est atténué près de leurs limites. Il n’y
a pas de découpage global par étages. Calcul et recherche du sol final restent
mis en cache par colonne ; seules les terres sèches utilisent cette passe.
La couverture végétale varie sur 135 blocs, au lieu de la superposition de
bruits à 49 et 13 blocs. Les surfaces trop pentues et les surplombs restent
en pierre jaune ; la terre et le sable occupent les parties suffisamment
douces et soutenues. Arbres écorcés supprimés du nouveau plan. Palmiers,
chênes feuillus, petits buissons, fleurs, cactus et herbes sèches conservés.
La nouvelle pierre utilise les propriétés physiques et sons de la stone,
la teinte cartographique du sable et le tag de minage à la pioche. Elle se
ramasse elle-même, possède un BlockItem et les noms « Pierre jaune » /
« Yellow Stone ». Teinte de bloc, particules et inventaire : `#F5D890` sur
le grain natif de stone, avec six faces et rotations du modèle. Les actifs
sont fournis par le mod ; aucune dépendance à un pack externe ni texture
PNG supplémentaire. Cette première pierre n’ajoute pas de variantes taillées.
## Vérifications du 5 octobre 2026
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=base204,west220,west221 -PsanctuaryAtlasOnly=true` :
réussi, six GameTests obligatoires passés. Le contrôle 221 couvre les graines
0, 42, −6763683964522674921, 3285501279717306940,
−8263140775942472950 et −189784416336457674. Volume marin comparé à 220,
réduction des creux de ravins et des transitions de couverture, présence des
cavités 3D, roche jaune et végétation, absence de bois écorcé, plage libre et
positions des structures conservées. Ancien profil 220 vérifié séparément.
Le second test vérifie la pioche en bois, le butin exact du bloc, son BlockItem
et la reconstruction des deux onglets créatifs natifs avec Pierre jaune juste
après Stone. Les libellés FR/EN, modèle à six faces teintées, modèle d’inventaire,
tag pioche et table de butin sont également vérifiés dans le JAR livré.
MRpack intègre, JAR identique au build, profils historiques et trois tailles
présents ; aucun monde ni module de test. Copie dans Downloads.
SHA-256 MRpack : `9128cdb4640cdf1bba81ee64bd6998c39bf0b7aaa07617440f946c1983c53024`.
SHA-256 JAR : `cc91b110c7289da152929d4a3ddd9110015d1f100c1071067954a476e308a3f0`.
Traces : `build/west221-build.log`, `build/west221-mrpack-receipt.json`.
Essais natifs Small sur 42 et 0 : activation Ouest, interruption à 0/94 chunks
et reprise jusqu’à READY réussies, 94 chunks préparés par expansion de 1024.
| Graine | Centre Ouest | Graine de l’expansion | Palmiers | Colonnes de plage contrôlées | Blocs de pierre jaune contrôlés |
| --- | --- | --- | --- | --- | --- |
| 42 | −960, 0 | −8263140775942472950 | 212 | 3207 | 63 |
| 0 | −944, 128 | −189784416336457674 | 181 | 2688 | 61 |
Huit troncs et 18 décors de six catégories inspectés physiquement par monde,
survie native des plantes vérifiée, aucun bois écorcé dans les plans. Monument,
relais, quatre épaves et douze coffres conservés ; eau et falaises immergées
contrôlées. Le terrain marin est comparé, pas la position exacte de chaque
corail natif. Medium/Large sont assemblés mais n’ont pas reçu de nouvelle
visite native pour cette livraison. Aucun ancien monde modifié ou régénéré.
Rapports : `build/worldgen-lab/west221-final/small/42/{cold,warm}.json` et
`build/worldgen-lab/west221-final0/small/0/{cold,warm}.json`.
Visite neuve `Sanctuary-West-221-42` ouverte le 5 octobre 2026 à 16:31:00,
Vulkan, distance 32 chunks et simulation 5, créatif/vol/commandes. Le client
vérifie les six faces teintées et la texture stone native :
`YELLOW_STONE221_CLIENT_PASS faces=6 tint=fff5d890`, puis
`WEST221_VISIT_OPEN`. Cela valide le chargement du modèle, pas une appréciation
visuelle des nouvelles parois. Trace :
`build/worldgen-lab/west221-visit/small/42/client/logs/latest.log`.
+48
View File
@@ -0,0 +1,48 @@
# Roche jaune — beta.222
## Contrat
Demande du 5 octobre 2026 : renommer « Pierre jaune » en « Roche jaune » et
ajouter les déclinaisons escaliers et dalles, intégrées aux menus Minecraft.
Branche `codex/yellow-rock-variants-beta222`, base `a3dd17c` (beta.221).
Le bloc conserve `sanctuary:yellow_stone`. Les nouveaux identifiants sont
`sanctuary:yellow_stone_stairs` et `sanctuary:yellow_stone_slab`.
Aucun changement de terrain, de profil de génération (221), de format de
sauvegarde ni d'expansion. Aucune modification de monde existant.
## Mise en œuvre
- FR : Roche jaune, Escalier en roche jaune, Dalle en roche jaune.
- EN : Yellow Rock, Yellow Rock Stairs, Yellow Rock Slab.
- Bloc dans Naturel juste après Stone ; famille complète dans Construction
juste après Stone, dans l'ordre bloc, escalier, dalle.
- Texture et teinte du bloc sur les deux variantes, minage à la pioche,
butin d'une variante ou deux dalles pour une dalle double.
- Établi : six blocs donnent quatre escaliers ; trois blocs donnent six dalles.
- Tailleur de pierre : un bloc donne un escalier ou deux dalles.
- Recettes débloquées dans le livre en obtenant de la roche jaune.
## Vérifications du 5 octobre 2026
`./gradlew check build assemblePack :sanctuary-test:exportDuoLaunch
-PsanctuaryFocusedTests=rock222,west221 -PsanctuaryAtlasOnly=true` : réussi,
cinq GameTests passés. Pose et fusion de dalles, coin d’escalier, eau, pioche,
butin simple/double, recettes natives, déblocage du livre et ordre créatif.
Le contrôle du relief 221 reste vert. Une erreur d’import dans le nouveau
GameTest a été corrigée avant ce passage réussi.
Test client ciblé `runClientGameTest` avec `sanctuaryYellowRock222ClientTests`
et backend Vulkan : réussi à 16:49:51. Les 87 états bloc/escalier/dalle ont
la texture stone native, la teinte jaune et le même nombre de faces que leur
équivalent vanilla. Aucun monde ouvert pour ce contrôle de modèles.
Les cinq géométries source, UV et culling sont également comparés aux modèles
Minecraft 26.3. Il ne s’agit pas d’une nouvelle validation visuelle du paysage.
MRpack exporté et vérifié : ZIP intègre, JAR exact, nom FR, modèles teintés,
recettes et trois tailles 221 ; aucun monde ni module de test. Copie dans
`/Users/koka/Downloads/Sanctuary-beta.222.mrpack`.
SHA-256 : `d9ed126f6f6ae2fd425520dae99eafcd91295c2cdde75398f9a2bc929711e005`.
Traces : `build/rock222-build-final.log`, `build/rock222-client.log` et
`build/rock222-mrpack-receipt.json`. Aucun canal public ni instance personnelle
mis à jour. La visite 221 reste ouverte jusqu’au prochain labo Est demandé.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.203
pack_version=beta.203
mod_version=beta.233
pack_version=beta.233
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+5 -5
View File
@@ -1,11 +1,11 @@
# Demeure
Mod Fabric autonome pour Minecraft 26.3-pre-2, porté depuis les sources
Service interne à Sanctuary depuis beta.204, pour Minecraft 26.3, porté depuis les sources
historiques référencées par Sanctuary 26.2. Il mesure les empreintes du vivant
et l'attachement aux lieux ; il n'attribue ni propriété ni protection.
Sanctuary l'embarque dans son JAR et utilise son API pour la surcouche de
l'Atlas. Demeure ne dépend pas de Sanctuary. L'hôte peut restreindre les joueurs
Les sources vivent dans `mods/sanctuary/src/main/java/fr/koka/demeure/`.
Sanctuary utilise ce service pour la surcouche de l’Atlas et restreint les joueurs
éligibles avec `DemeureService.setEligibility` ; le pack limite ainsi les
traces aux mondes dont la progression est active et saine.
@@ -27,6 +27,6 @@ une erreur serveur ; le fichier existant est conservé. Les limites sont
explicitées dans [le contrat beta.006](../../docs/demeure-beta006.md).
Les données de l'ancien Demeure ne sont pas importées automatiquement.
`./gradlew :demeure:check :demeure:build` depuis la racine vérifie le modèle
et construit le module. Les tests natifs des événements et de l'Atlas vivent
`./gradlew :sanctuary:demeureSmoke :sanctuary:check` depuis la racine vérifie le modèle
et son intégration. Aucun JAR Demeure séparé ne doit être installé avec beta.204. Les tests natifs des événements et de l'Atlas vivent
dans Sanctuary. Licence GPL-3.0-or-later ; [provenance](PROVENANCE.md).
-27
View File
@@ -1,27 +0,0 @@
plugins { id 'net.fabricmc.fabric-loom' }
group = 'fr.koka.demeure'
version = rootProject.mod_version
base { archivesName = 'demeure' }
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 {
def values = [version: project.version, minecraft: rootProject.minecraft_version.replace('-pre-', '-pre.'), loader: rootProject.loader_version]
inputs.properties(values)
filesMatching('fabric.mod.json') { expand(values) }
}
tasks.named('jar') {
from(rootProject.file('LICENSE')) { rename { 'LICENSE_demeure' } }
from('PROVENANCE.md')
}
tasks.named('test') { failOnNoDiscoveredTests = false }
tasks.register('demeureSmoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.demeure.DemeureSmoke'
}
tasks.named('check') { dependsOn('demeureSmoke') }
@@ -1,13 +0,0 @@
{
"schemaVersion": 1,
"id": "demeure",
"version": "${version}",
"name": "Demeure",
"description": "Living footprints, activity and attachment to places. Sanctuary adaptation.",
"authors": ["KOKA99CAB", "Koka"],
"license": "GPL-3.0-or-later",
"environment": "*",
"entrypoints": {"main": ["fr.koka.demeure.DemeureService"]},
"mixins": ["demeure.mixins.json"],
"depends": {"fabricloader": ">=${loader}", "minecraft": "${minecraft}", "java": ">=25", "fabric-api": "*"}
}
@@ -0,0 +1,89 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ChunkPos;
/** Explicit disposable-lab preparation; never runs in the normal pack or a player's save. */
final class AllIslands231 {
private static final TicketType TICKET=new TicketType(0L,2|8);
private static final int[] CARDINALS={0,2,4,6};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;
int cardinal,index,done;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<ChunkPos> chunks;Map<ChunkPos,CompletableFuture<?>> pending=new LinkedHashMap<>();
}
static void register(){
Registry.register(BuiltInRegistries.TICKET_TYPE,SanctuaryMod.id("lab_all_islands231"),TICKET);
ServerTickEvents.END_SERVER_TICK.register(AllIslands231::tick);
}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started){
require(Boolean.getBoolean("sanctuary.worldgenLab.allIslands"),"Explicit all-islands option required");
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==231&&SharedIsland196.size(level.getChunkSource().getGenerator())==SharedIsland196.Size.LARGE,"Expected new 1024 lab");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;run=r;request(r);
}
private static void request(Run r){
var level=r.server.overworld();int sector=CARDINALS[r.cardinal];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing cardinal anchor");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
var level=server.overworld();var session=ExpansionRuntime.session(level);var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
if(r.chunks==null){
if(!r.request.isDone())return;
var admitted=r.request.join();var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Expansion preparation failed: "+session.progress());return;}
require(r.payment.getCount()==1&&island.diameter()==1024&&island.direction().equals(ExpansionIsland.DIRECTIONS.get(CARDINALS[r.cardinal])),"Wrong cardinal offer");
var relay=adapter.relay(island);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK),"Missing cardinal relay");
SanctuaryMod.LOGGER.info("ALL_ISLANDS231_ACTIVATED {} {}",island.direction(),relay);
if(++r.cardinal<CARDINALS.length){request(r);return;}
var islands=session.islands();require(islands.size()==9&&islands.stream().allMatch(i->i.status().equals("ready")&&i.diameter()==1024),"Expected nine ready 1024 islands");
var chunks=new LinkedHashSet<ChunkPos>();var manifest=new ArrayList<Map<String,Object>>();
for(var i:islands){
int radius=i.terrainRadius()+ExpansionIsland.DECORATION_MARGIN,count=0;
// Includes the rocky perimeter and decoration margin, not merely the nominal surface.
for(int x=(i.centerX()-radius)>>4;x<=(i.centerX()+radius)>>4;x++)for(int z=(i.centerZ()-radius)>>4;z<=(i.centerZ()+radius)>>4;z++)
if(i.ownsChunk(x,z)){chunks.add(new ChunkPos(x,z));count++;}
var data=new LinkedHashMap<String,Object>();data.put("id",i.id());data.put("direction",i.origin()?"center":i.direction());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("fullChunks",count);
if(!i.origin())data.put("relay",adapter.relay(i).toShortString());manifest.add(data);
}
r.chunks=List.copyOf(chunks);r.result.put("allIslands",manifest);r.result.put("pregenerationTarget",r.chunks.size());
SanctuaryMod.LOGGER.info("ALL_ISLANDS231_PREGEN_BEGIN chunks={}",r.chunks.size());
}
var source=level.getChunkSource();
for(var it=r.pending.entrySet().iterator();it.hasNext();){
var entry=it.next();if(!entry.getValue().isDone())continue;entry.getValue().join();var cp=entry.getKey();
require(source.getChunkNow(cp.x(),cp.z())!=null,"Chunk did not reach FULL: "+cp);
source.removeTicketWithRadius(TICKET,cp,0);it.remove();r.done++;
if(r.done%256==0)SanctuaryMod.LOGGER.info("ALL_ISLANDS231_PREGEN {}/{}",r.done,r.chunks.size());
}
while(r.pending.size()<4&&r.index<r.chunks.size()){
var cp=r.chunks.get(r.index++);r.pending.put(cp,source.addTicketAndLoadWithRadius(TICKET,cp,0));
}
if(r.done==r.chunks.size()){
server.saveEverything(false,true,true);r.result.put("pregeneratedFullChunks",r.done);r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);
run=null;WorldgenDiagnostics.writeReport(r.output,r.result);
}
}catch(Throwable error){
run=null;for(var cp:r.pending.keySet())server.overworld().getChunkSource().removeTicketWithRadius(TICKET,cp,0);
SanctuaryMod.LOGGER.error("ALL_ISLANDS231_FAILED",error);server.halt(false);
}
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,98 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Four real offers, interruption/reload, local generation and shoreline planting on fresh lab worlds. */
final class Diagonal225Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal225Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==225,"Wrong profile");result.put("verificationScope","diagonal225");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain225.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain225.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain225.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain225.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=190;y<304;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
}
habitatCounts.put(entry.getKey(),plants);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().endsWith("east")){
require(wetSample!=null,"No wet depression");level.getChunkAt(wetSample);
require(level.getFluidState(wetSample).is(net.minecraft.tags.FluidTags.WATER),"Dry wetland at "+wetSample);
// A mangrove biome includes dry banks too. Check the actual depression, not just its first habitat sample.
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL225_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL225_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,114 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Four real offers, interruption/reload, local generation and shoreline planting on fresh lab worlds. */
final class Diagonal226Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal226Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==226,"Wrong profile");result.put("verificationScope","diagonal226");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain226.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain226.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain226.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain226.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
}
habitatCounts.put(entry.getKey(),plants);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().equals("northeast")){
require(wetSample!=null,"No wet depression");level.getChunkAt(wetSample);
require(level.getFluidState(wetSample).is(net.minecraft.tags.FluidTags.WATER),"Dry wetland at "+wetSample);
// A mangrove biome includes dry banks too. Check the actual depression, not just its first habitat sample.
}
int floodedTrees=0;
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-420;dx<=420;dx+=24)for(int dz=-420;dz<=420;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(!c.water()||c.top()<DiagonalTerrain226.WATER-2)continue;
var cp=new ChunkPos(x>>4,z>>4);if(!visited.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++){
var before=f.column(xx,zz);if(!before.water())continue;
var root=new BlockPos(xx,before.top()+1,zz);
if(chunk.getBlockState(root).is(net.minecraft.tags.BlockTags.LOGS))floodedTrees++;
}
if(floodedTrees>=3||visited.size()>=24)break search;
}
require(floodedTrees>0,"No native tree rooted inside the shallow marsh");
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("floodedTrees",floodedTrees);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL226_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL226_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,152 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Four real offers, interruption/reload, local generation and shoreline planting on fresh lab worlds. */
final class Diagonal227Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal227Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==227,"Wrong profile");result.put("verificationScope","diagonal227");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain227.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain227.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain227.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain227.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
}
habitatCounts.put(entry.getKey(),plants);
}
// A savanna is sparse: one random chunk per habitat is not a forest census.
// If the first samples are treeless, inspect additional supported savanna chunks.
if(i.direction().equals("southwest")&&(logs==0||leaves==0)){
var checked=new HashSet<ChunkPos>(samples.values());int extra=0;
search:for(int dx=-360;dx<=360;dx+=24)for(int dz=-360;dz<=360;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(c.top()<120||!c.biome().startsWith("savanna"))continue;
var cp=new ChunkPos(x>>4,z>>4);if(!checked.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=80;y<280;y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(net.minecraft.tags.BlockTags.LOGS))logs++;if(state.is(net.minecraft.tags.BlockTags.LEAVES))leaves++;
}
extra++;if(logs>0&&leaves>0||extra>=18)break search;
}
habitatCounts.put("additionalSavannaChunks",extra);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().equals("northeast")){
require(wetSample!=null,"No wet depression");level.getChunkAt(wetSample);
require(level.getFluidState(wetSample).is(net.minecraft.tags.FluidTags.WATER),"Dry wetland at "+wetSample);
// A mangrove biome includes dry banks too. Check the actual depression, not just its first habitat sample.
}
int floodedTrees=0;
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-420;dx<=420;dx+=24)for(int dz=-420;dz<=420;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(!c.water()||c.top()<DiagonalTerrain227.WATER-2)continue;
var cp=new ChunkPos(x>>4,z>>4);if(!visited.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++){
var before=f.column(xx,zz);if(!before.water())continue;
var root=new BlockPos(xx,before.top()+1,zz);
if(chunk.getBlockState(root).is(net.minecraft.tags.BlockTags.LOGS))floodedTrees++;
}
if(floodedTrees>=3||visited.size()>=24)break search;
}
require(floodedTrees>0,"No native tree rooted inside the shallow marsh");
}
var caveCounts=new TreeMap<String,Integer>();
if(i.direction().equals("southeast")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<150)continue;
for(int y=Math.max(48,c.bottom()+8);y<Math.min(312,c.top()-32);y+=12){
var biome=f.biome(x,y,z);if(!biome.endsWith("227")||f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of("sanctuary:diagonal_lush227","sanctuary:diagonal_dripstone227")){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<Math.min(320,f.top(x,z)-28);y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(biome.endsWith("lush227")?(state.is(Blocks.MOSS_BLOCK)||state.is(Blocks.CAVE_VINES)||state.is(Blocks.CAVE_VINES_PLANT)||state.is(Blocks.SPORE_BLOSSOM)):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>20||++checked>=16)break;
}
require(blocks>20,"Missing native cave vegetation "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("floodedTrees",floodedTrees);data.put("caveBlocks",caveCounts);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL227_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL227_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,152 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Four real offers, interruption/reload, local generation and shoreline planting on fresh lab worlds. */
final class Diagonal228Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal228Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==228,"Wrong profile");result.put("verificationScope","diagonal228");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain228.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain228.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain228.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain228.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
}
habitatCounts.put(entry.getKey(),plants);
}
// A savanna is sparse: one random chunk per habitat is not a forest census.
// If the first samples are treeless, inspect additional supported savanna chunks.
if(i.direction().equals("southwest")&&(logs==0||leaves==0)){
var checked=new HashSet<ChunkPos>(samples.values());int extra=0;
search:for(int dx=-360;dx<=360;dx+=24)for(int dz=-360;dz<=360;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(c.top()<120||!c.biome().startsWith("savanna"))continue;
var cp=new ChunkPos(x>>4,z>>4);if(!checked.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=80;y<280;y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(net.minecraft.tags.BlockTags.LOGS))logs++;if(state.is(net.minecraft.tags.BlockTags.LEAVES))leaves++;
}
extra++;if(logs>0&&leaves>0||extra>=18)break search;
}
habitatCounts.put("additionalSavannaChunks",extra);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().equals("northeast")){
require(wetSample!=null,"No wet depression");level.getChunkAt(wetSample);
require(level.getFluidState(wetSample).is(net.minecraft.tags.FluidTags.WATER),"Dry wetland at "+wetSample);
// A mangrove biome includes dry banks too. Check the actual depression, not just its first habitat sample.
}
int floodedTrees=0;
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-420;dx<=420;dx+=24)for(int dz=-420;dz<=420;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(!c.water()||c.top()<DiagonalTerrain228.WATER-2)continue;
var cp=new ChunkPos(x>>4,z>>4);if(!visited.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++){
var before=f.column(xx,zz);if(!before.water())continue;
var root=new BlockPos(xx,before.top()+1,zz);
if(chunk.getBlockState(root).is(net.minecraft.tags.BlockTags.LOGS))floodedTrees++;
}
if(floodedTrees>=3||visited.size()>=24)break search;
}
require(floodedTrees>0,"No native tree rooted inside the shallow marsh");
}
var caveCounts=new TreeMap<String,Integer>();
if(i.direction().equals("southeast")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<150)continue;
for(int y=Math.max(48,c.bottom()+8);y<Math.min(312,c.top()-32);y+=12){
var biome=f.biome(x,y,z);if(!biome.endsWith("227")||f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of("sanctuary:diagonal_lush227","sanctuary:diagonal_dripstone227")){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<Math.min(320,f.top(x,z)-28);y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(biome.endsWith("lush227")?(state.is(Blocks.MOSS_BLOCK)||state.is(Blocks.CAVE_VINES)||state.is(Blocks.CAVE_VINES_PLANT)||state.is(Blocks.SPORE_BLOSSOM)):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>20||++checked>=16)break;
}
require(blocks>20,"Missing native cave vegetation "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("floodedTrees",floodedTrees);data.put("caveBlocks",caveCounts);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL228_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL228_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,174 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Real offers and reload with native lower pale trees, upper fireflies and dry cave features. */
final class Diagonal229Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal229Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==229,"Wrong profile");result.put("verificationScope","diagonal229");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain229.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain229.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain229.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain229.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
}
habitatCounts.put(entry.getKey(),plants);
}
// A savanna is sparse: one random chunk per habitat is not a forest census.
// If the first samples are treeless, inspect additional supported savanna chunks.
if(i.direction().equals("southwest")&&(logs==0||leaves==0)){
var checked=new HashSet<ChunkPos>(samples.values());int extra=0;
search:for(int dx=-360;dx<=360;dx+=24)for(int dz=-360;dz<=360;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(c.top()<120||!c.biome().startsWith("savanna"))continue;
var cp=new ChunkPos(x>>4,z>>4);if(!checked.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=80;y<280;y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(net.minecraft.tags.BlockTags.LOGS))logs++;if(state.is(net.minecraft.tags.BlockTags.LEAVES))leaves++;
}
extra++;if(logs>0&&leaves>0||extra>=18)break search;
}
habitatCounts.put("additionalSavannaChunks",extra);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
int paleLogs=0,fireflies=0,paleChunks=0;
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){
var cp=new ChunkPos((i.centerX()+dx)>>4,(i.centerZ()+dz)>>4);
if(DiagonalVegetation229.paleSites(f,cp).isEmpty()||!visited.add(cp))continue;
var chunk=level.getChunk(cp.x(),cp.z());paleChunks++;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<448;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(Blocks.PALE_OAK_LOG)){require(y<f.top(x,z),"Pale oak outside lower habitat");paleLogs++;}
if(state.is(Blocks.FIREFLY_BUSH)){require(y==f.top(x,z)+1,"Firefly bush in lower pale habitat");fireflies++;}
if(state.is(Blocks.WATER)){
var at=p.immutable();require(f.pool(cp).contains(at),"Unplanned water in the wetland at "+at);
require(chunk.getBlockState(at.below()).isSolidRender(),"Water without natural floor");
for(var dir:Direction.Plane.HORIZONTAL){var side=at.relative(dir);require(f.pool(cp).contains(side)||chunk.getBlockState(side).isSolidRender(),"Leaking natural pool");}
}
}
if(paleLogs>20&&fireflies>0||paleChunks>=24)break search;
}
require(paleLogs>0&&fireflies>0,"Missing pale trees or upper fireflies: "+paleLogs+"/"+fireflies+" chunks="+paleChunks);
}
var caveCounts=new TreeMap<String,Integer>();
if(i.direction().equals("southeast")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<150)continue;
for(int y=Math.max(48,c.bottom()+8);y<Math.min(312,c.top()-32);y+=12){
var biome=f.biome(x,y,z);if(!biome.endsWith("227")||f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of("sanctuary:diagonal_lush227","sanctuary:diagonal_dripstone227")){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<Math.min(320,f.top(x,z)-28);y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(biome.endsWith("lush227")?(state.is(Blocks.MOSS_BLOCK)||state.is(Blocks.CAVE_VINES)||state.is(Blocks.CAVE_VINES_PLANT)||state.is(Blocks.SPORE_BLOSSOM)):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>20||++checked>=16)break;
}
require(blocks>20,"Missing native cave vegetation "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
if(i.direction().equals("southwest")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
for(int y=Math.max(40,c.bottom());y<c.top()-12;y+=8){
String biome=f.biome(x,y,z);if(f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of(DiagonalTerrain229.SULFUR,DiagonalTerrain229.DRIP)){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<f.top(x,z)-12;y++){
var state=chunk.getBlockState(p.set(x,y,z));
require(!state.is(Blocks.MOSS_BLOCK)&&!state.is(Blocks.CAVE_VINES)&&!state.is(Blocks.SPORE_BLOSSOM),"Lush decoration in southwest");
if(biome.equals(DiagonalTerrain229.SULFUR)?state.is(Blocks.SULFUR_SPIKE):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>5||++checked>=24)break;
}
require(blocks>0,"Missing native dry cave decoration "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("paleLogs",paleLogs);data.put("upperFireflies",fireflies);data.put("paleChunks",paleChunks);data.put("caveBlocks",caveCounts);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL229_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL229_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,194 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Real offers and reload with native lower pale trees, upper fireflies and dry cave features. */
final class Diagonal230Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal230Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==230,"Wrong profile");result.put("verificationScope","diagonal230");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain230.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain230.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
int paleLogs=0,fireflies=0,paleChunks=0,openPaleRoots=0,neDrip=0,yellowRock=0,greyStone=0;
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain230.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain230.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
if(i.direction().equals("northeast")&&state.is(Blocks.FIREFLY_BUSH)){
require(y==f.top(x,z)+1&&!f.lower(x,y,z),"Firefly bush in lower habitat");fireflies++;
}
}
habitatCounts.put(entry.getKey(),plants);
}
// A savanna is sparse: one random chunk per habitat is not a forest census.
// If the first samples are treeless, inspect additional supported savanna chunks.
if(i.direction().equals("southwest")&&(logs==0||leaves==0)){
var checked=new HashSet<ChunkPos>(samples.values());int extra=0;
search:for(int dx=-360;dx<=360;dx+=24)for(int dz=-360;dz<=360;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(c.top()<120||!c.biome().startsWith("savanna"))continue;
var cp=new ChunkPos(x>>4,z>>4);if(!checked.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=80;y<280;y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(net.minecraft.tags.BlockTags.LOGS))logs++;if(state.is(net.minecraft.tags.BlockTags.LEAVES))leaves++;
}
extra++;if(logs>0&&leaves>0||extra>=18)break search;
}
habitatCounts.put("additionalSavannaChunks",extra);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){
var cp=new ChunkPos((i.centerX()+dx)>>4,(i.centerZ()+dz)>>4);
if(DiagonalVegetation230.paleSites(f,cp).isEmpty()||!visited.add(cp))continue;
var chunk=level.getChunk(cp.x(),cp.z());paleChunks++;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<448;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(Blocks.PALE_OAK_LOG)){
paleLogs++;if(chunk.getBlockState(p.below()).is(Blocks.DIRT)){require(f.lower(x,y,z),"Pale oak rooted on upper plateau");if(y==f.top(x,z)+1)openPaleRoots++;}
}
if(state.is(Blocks.POINTED_DRIPSTONE))neDrip++;
if(state.is(Blocks.FIREFLY_BUSH)){require(y==f.top(x,z)+1&&!f.lower(x,y,z),"Firefly bush in lower pale habitat");fireflies++;}
if(state.is(Blocks.WATER)){
var at=p.immutable();require(f.pool(cp).contains(at),"Unplanned water in the wetland at "+at);
require(chunk.getBlockState(at.below()).isSolidRender(),"Water without natural floor");
for(var dir:Direction.Plane.HORIZONTAL){var side=at.relative(dir);require(f.pool(cp).contains(side)||chunk.getBlockState(side).isSolidRender(),"Leaking natural pool");}
}
}
if(paleLogs>20&&openPaleRoots>0&&neDrip>0&&fireflies>0||paleChunks>=32)break search;
}
require(paleLogs>0&&openPaleRoots>0&&neDrip>0&&fireflies>0,"Missing lower open trees/dripstone/upper fireflies: "+paleLogs+"/"+openPaleRoots+"/"+neDrip+"/"+fireflies+" chunks="+paleChunks);
}
var caveCounts=new TreeMap<String,Integer>();
if(i.direction().equals("southeast")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<150)continue;
for(int y=Math.max(48,c.bottom()+8);y<Math.min(312,c.top()-32);y+=12){
var biome=f.biome(x,y,z);if(!biome.endsWith("227")||f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of("sanctuary:diagonal_lush227","sanctuary:diagonal_dripstone227")){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<Math.min(320,f.top(x,z)-28);y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(biome.endsWith("lush227")?(state.is(Blocks.MOSS_BLOCK)||state.is(Blocks.CAVE_VINES)||state.is(Blocks.CAVE_VINES_PLANT)||state.is(Blocks.SPORE_BLOSSOM)):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>20||++checked>=16)break;
}
require(blocks>20,"Missing native cave vegetation "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
if(i.direction().equals("southwest")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
for(int y=Math.max(40,c.bottom());y<c.top()-12;y+=8){
String biome=f.biome(x,y,z);if(f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of(DiagonalTerrain230.SULFUR,DiagonalTerrain230.DRIP)){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<f.top(x,z)-12;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellowRock++;if(state.is(Blocks.STONE))greyStone++;
require(!state.is(Blocks.MOSS_BLOCK)&&!state.is(Blocks.CAVE_VINES)&&!state.is(Blocks.SPORE_BLOSSOM),"Lush decoration in southwest");
if(biome.equals(DiagonalTerrain230.SULFUR)?state.is(Blocks.SULFUR_SPIKE):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>5||++checked>=24)break;
}
require(blocks>0,"Missing native dry cave decoration "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
if(i.direction().equals("southwest")){
int extra=0;
search:for(int dx=-360;dx<=360;dx+=48)for(int dz=-360;dz<=360;dz+=48){
if(yellowRock>100&&greyStone>100)break search;
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<140||!f.state(x,c.top()-8,z).is(Blocks.STONE))continue;
var cp=new ChunkPos(x>>4,z>>4);var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=40;y<f.top(xx,zz);y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellowRock++;if(state.is(Blocks.STONE))greyStone++;
}
if(++extra>=12)break search;
}
require(yellowRock>100&&greyStone>100,"Missing mixed savanna geology");
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("paleLogs",paleLogs);data.put("openPaleRoots",openPaleRoots);data.put("neDripstone",neDrip);data.put("yellowRock",yellowRock);data.put("greyStone",greyStone);data.put("upperFireflies",fireflies);data.put("paleChunks",paleChunks);data.put("caveBlocks",caveCounts);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL230_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL230_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,200 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.ChunkPos;
/** Real offers and reload with native lower pale trees, upper fireflies and dry cave features. */
final class Diagonal231Checks {
private static final int[] SECTORS={1,3,5,7};
private static Run run;
private static final class Run {
MinecraftServer server;Path output;Map<String,Object> result;long started;boolean fresh;
int index;CompletableFuture<ExpansionIsland> request;ItemStack payment;
List<Map<String,Object>> islands=new ArrayList<>();
}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Diagonal231Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==231,"Wrong profile");result.put("verificationScope","diagonal231");
var r=new Run();r.server=server;r.output=output;r.result=result;r.started=started;r.fresh=fresh;
if(fresh)request(r);else{
var saved=ExpansionRuntime.session(level).islands();require(saved.size()==2,"Reservation lost");
r.request=CompletableFuture.completedFuture(saved.get(1));r.payment=ItemStack.EMPTY;
}
run=r;
}
private static void request(Run r){
var level=r.server.overworld();int sector=SECTORS[r.index];
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var bad=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,bad,()->true).isCompletedExceptionally()&&bad.getCount()==64,"Wrong payment accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);r.payment=new ItemStack(demand.item(),demand.count()+1);
r.request=RelayOffers199.offer(level,pos,r.payment,()->true);
require(RelayOffers199.offer(level,pos,r.payment,()->true).isCompletedExceptionally(),"Duplicate offer accepted");
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server!=server)return;
try{
require(System.nanoTime()-r.started<java.util.concurrent.TimeUnit.MINUTES.toNanos(12),"Timeout");if(!r.request.isDone())return;
var admitted=r.request.join();var level=server.overworld();var session=ExpansionRuntime.session(level);var a=LabExpansions199.get(level.getChunkSource().getGenerator());
require(DiagonalTerrain231.active(admitted)&&ExpansionIsland.DIRECTIONS.indexOf(admitted.direction())==SECTORS[r.index],"Wrong diagonal/size");
require(admitted.climate().equals(DiagonalTerrain225.climate(admitted.direction())),"Wrong humidity axis");
if(r.fresh){
require(r.payment.getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");
r.result.put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
require(a.preparationChunks(i).size()==49,"Unbounded preparation");var f=DiagonalTerrain231.field(i);var relay=a.relay(i);
require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==SECTORS[r.index],"Missing/wrong relay");
try{RelayOffers199.offer(level,relay,new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Unusable relay");}
if(r.index>0)require(r.payment.getCount()==1,"Incorrect offer cost");
int grass=0,flowers=0,carpets=0,hanging=0;
int paleLogs=0,fireflies=0,paleChunks=0,openPaleRoots=0,neDrip=0,yellowRock=0,greyStone=0;
var samples=new TreeMap<String,ChunkPos>();BlockPos wetSample=null;
for(int dx=-400;dx<=400;dx+=24)for(int dz=-400;dz<=400;dz+=24){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()>50&&c.top()>0)samples.putIfAbsent(c.biome(),new ChunkPos(x>>4,z>>4));if(wetSample==null&&c.water()&&c.top()<DiagonalTerrain230.WATER-2)wetSample=new BlockPos(x,DiagonalTerrain230.WATER,z);}
int logs=0,leaves=0,water=0,cactus=0;var p=new BlockPos.MutableBlockPos();var habitatCounts=new TreeMap<String,Integer>();
for(var entry:samples.entrySet()){
var cp=entry.getValue();var chunk=level.getChunk(cp.x(),cp.z());int plants=0;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<480;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(net.minecraft.tags.BlockTags.LOGS)){logs++;plants++;}
if(state.is(net.minecraft.tags.BlockTags.LEAVES)){leaves++;plants++;}
if(state.is(Blocks.CACTUS)){cactus++;plants++;}
if(state.is(Blocks.WATER))water++;
if(i.direction().equals("northeast")&&state.is(Blocks.FIREFLY_BUSH)){
require(y==f.top(x,z)+1&&!f.lower(x,y,z),"Firefly bush in lower habitat");fireflies++;
}
}
habitatCounts.put(entry.getKey(),plants);
}
// A savanna is sparse: one random chunk per habitat is not a forest census.
// If the first samples are treeless, inspect additional supported savanna chunks.
if(i.direction().equals("southwest")&&(logs==0||leaves==0)){
var checked=new HashSet<ChunkPos>(samples.values());int extra=0;
search:for(int dx=-360;dx<=360;dx+=24)for(int dz=-360;dz<=360;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
if(c.top()<120||!c.biome().startsWith("savanna"))continue;
var cp=new ChunkPos(x>>4,z>>4);if(!checked.add(cp))continue;var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=80;y<280;y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(net.minecraft.tags.BlockTags.LOGS))logs++;if(state.is(net.minecraft.tags.BlockTags.LEAVES))leaves++;
}
extra++;if(logs>0&&leaves>0||extra>=18)break search;
}
habitatCounts.put("additionalSavannaChunks",extra);
}
require(logs>0&&leaves>0,"No native forest on "+i.direction()+" "+habitatCounts);
if(i.direction().equals("northeast")){
var visited=new HashSet<ChunkPos>();
search:for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){
var cp=new ChunkPos((i.centerX()+dx)>>4,(i.centerZ()+dz)>>4);
if(DiagonalVegetation230.paleSites(f,cp).isEmpty()||!visited.add(cp))continue;
var chunk=level.getChunk(cp.x(),cp.z());paleChunks++;
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=32;y<448;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(Blocks.PALE_OAK_LOG)){
paleLogs++;if(chunk.getBlockState(p.below()).is(Blocks.DIRT)){require(f.lower(x,y,z),"Pale oak rooted on upper plateau");if(y==f.top(x,z)+1)openPaleRoots++;}
}
if(state.is(Blocks.POINTED_DRIPSTONE))neDrip++;
if(state.is(Blocks.SHORT_GRASS)||state.is(Blocks.TALL_GRASS))grass++;
if(state.is(Blocks.CLOSED_EYEBLOSSOM)||state.is(Blocks.OPEN_EYEBLOSSOM))flowers++;
if(state.is(Blocks.PALE_MOSS_CARPET))carpets++;
if(state.is(Blocks.PALE_HANGING_MOSS))hanging++;
if(state.is(Blocks.FIREFLY_BUSH)){require(y==f.top(x,z)+1&&!f.lower(x,y,z),"Firefly bush in lower pale habitat");fireflies++;}
if(state.is(Blocks.WATER)){
var at=p.immutable();require(f.pool(cp).contains(at),"Unplanned water in the wetland at "+at);
require(chunk.getBlockState(at.below()).isSolidRender(),"Water without natural floor");
for(var dir:Direction.Plane.HORIZONTAL){var side=at.relative(dir);require(f.pool(cp).contains(side)||chunk.getBlockState(side).isSolidRender(),"Leaking natural pool");}
}
}
if(paleLogs>20&&openPaleRoots>0&&neDrip>0&&fireflies>0&&grass>0&&flowers>0&&carpets>0||paleChunks>=32)break search;
}
require(grass>0&&flowers>0&&carpets>0,"Missing understory: "+grass+"/"+flowers+"/"+carpets);
require(paleLogs>0&&openPaleRoots>0&&neDrip>0&&fireflies>0,"Missing lower open trees/dripstone/upper fireflies: "+paleLogs+"/"+openPaleRoots+"/"+neDrip+"/"+fireflies+" chunks="+paleChunks);
}
var caveCounts=new TreeMap<String,Integer>();
if(i.direction().equals("southeast")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<150)continue;
for(int y=Math.max(48,c.bottom()+8);y<Math.min(312,c.top()-32);y+=12){
var biome=f.biome(x,y,z);if(!biome.endsWith("227")||f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of("sanctuary:diagonal_lush227","sanctuary:diagonal_dripstone227")){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<Math.min(320,f.top(x,z)-28);y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(biome.endsWith("lush227")?(state.is(Blocks.MOSS_BLOCK)||state.is(Blocks.CAVE_VINES)||state.is(Blocks.CAVE_VINES_PLANT)||state.is(Blocks.SPORE_BLOSSOM)):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>20||++checked>=16)break;
}
require(blocks>20,"Missing native cave vegetation "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
if(i.direction().equals("southwest")){
var candidates=new TreeMap<String,LinkedHashSet<ChunkPos>>();
for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);
for(int y=Math.max(40,c.bottom());y<c.top()-12;y+=8){
String biome=f.biome(x,y,z);if(f.density(x,y,z)>0)continue;
candidates.computeIfAbsent(biome,k->new LinkedHashSet<>()).add(new ChunkPos(x>>4,z>>4));break;
}
}
for(String biome:List.of(DiagonalTerrain230.SULFUR,DiagonalTerrain230.DRIP)){
int blocks=0,checked=0;
for(var cp:candidates.getOrDefault(biome,new LinkedHashSet<>())){
var chunk=level.getChunk(cp.x(),cp.z());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<f.top(x,z)-12;y++){
var state=chunk.getBlockState(p.set(x,y,z));
if(state.is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellowRock++;if(state.is(Blocks.STONE))greyStone++;
require(!state.is(Blocks.MOSS_BLOCK)&&!state.is(Blocks.CAVE_VINES)&&!state.is(Blocks.SPORE_BLOSSOM),"Lush decoration in southwest");
if(biome.equals(DiagonalTerrain230.SULFUR)?state.is(Blocks.SULFUR_SPIKE):state.is(Blocks.POINTED_DRIPSTONE))blocks++;
}
if(blocks>5||++checked>=24)break;
}
require(blocks>0,"Missing native dry cave decoration "+biome+" chunks="+checked);caveCounts.put(biome,blocks);
}
}
if(i.direction().equals("southwest")){
int extra=0;
search:for(int dx=-360;dx<=360;dx+=48)for(int dz=-360;dz<=360;dz+=48){
if(yellowRock>100&&greyStone>100)break search;
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.top()<140||!f.state(x,c.top()-8,z).is(Blocks.STONE))continue;
var cp=new ChunkPos(x>>4,z>>4);var chunk=level.getChunk(cp.x(),cp.z());
for(int xx=cp.getMinBlockX();xx<=cp.getMaxBlockX();xx++)for(int zz=cp.getMinBlockZ();zz<=cp.getMaxBlockZ();zz++)for(int y=40;y<f.top(xx,zz);y++){
var state=chunk.getBlockState(p.set(xx,y,zz));if(state.is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellowRock++;if(state.is(Blocks.STONE))greyStone++;
}
if(++extra>=12)break search;
}
require(yellowRock>100&&greyStone>100,"Missing mixed savanna geology");
}
var data=new LinkedHashMap<String,Object>();data.put("direction",i.direction());data.put("id",i.id());data.put("diameter",i.diameter());data.put("x",i.centerX());data.put("z",i.centerZ());data.put("seed",i.seed());data.put("relay",relay.toShortString());data.put("biomes",habitatCounts);data.put("logs",logs);data.put("leaves",leaves);data.put("water",water);if(wetSample!=null)data.put("waterProbe",wetSample.toShortString());data.put("cactus",cactus);data.put("paleLogs",paleLogs);data.put("grass",grass);data.put("flowers",flowers);data.put("carpets",carpets);data.put("hangingMoss",hanging);data.put("openPaleRoots",openPaleRoots);data.put("neDripstone",neDrip);data.put("yellowRock",yellowRock);data.put("greyStone",greyStone);data.put("upperFireflies",fireflies);data.put("paleChunks",paleChunks);data.put("caveBlocks",caveCounts);data.put("preparedChunks",a.preparationChunks(i).size());r.islands.add(data);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL231_CHECKED {}",data);
if(++r.index<4){request(r);return;}
var source=Anchors189.source(level.getChunkSource().getGenerator());int cane=0;
for(var entry:SugarCane225.banks(source).entrySet()){
level.getChunk(entry.getKey().x(),entry.getKey().z());
for(var pos:entry.getValue())if(level.getBlockState(pos).is(Blocks.SUGAR_CANE)){
require(level.getBlockState(pos).canSurvive(level,pos),"Cane without water");cane++;
}
}
require(cane>5,"Missing island sugar cane: "+cane);r.result.put("sugarCaneColumns",cane);r.result.put("islands",r.islands);if(Boolean.getBoolean("sanctuary.worldgenLab.allIslands")){run=null;AllIslands231.begin(r.server,r.output,r.result,r.started);return;}finish(r);
}catch(Throwable error){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("DIAGONAL231_CHECK_FAILED",error);server.halt(false);}
}
private static void finish(Run r){run=null;r.result.put("readyForVisitMs",(System.nanoTime()-r.started)/1_000_000);WorldgenDiagnostics.writeReport(r.output,r.result);}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,99 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual East anchor transaction/restart, native jungle temple, sealed lava and native vegetation. */
final class East223Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(East223Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&(adapter.rootGeneration()==223||adapter.rootGeneration()==224),"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","east"+adapter.rootGeneration());
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==2).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing East anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(2);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(EastVolcano223.active(admitted)&&admitted.centerX()>0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong East placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=EastVolcano223.field(i);var temple=EastTemple223.plan(i);
require(a.preparationChunks(i).size()<=100,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int chests=0,traps=0,masonry=0;var pos=new BlockPos.MutableBlockPos();var bounds=temple.bounds();
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 state=level.getBlockState(pos.set(x,y,z));
if(state.is(Blocks.CHEST)&&level.getBlockEntity(pos) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)chests++;
if(state.is(Blocks.DISPENSER))traps++;
if(state.is(Blocks.MOSSY_COBBLESTONE)||state.is(Blocks.COBBLESTONE))masonry++;
}
require(chests==2&&traps==2&&masonry>300,"Native jungle temple incomplete "+chests+"/"+traps+"/"+masonry);
int lava=0,wallChecks=0;
for(int dx=-80;dx<=80;dx+=3)for(int dz=-80;dz<=80;dz+=3){
int x=i.centerX()+dx,z=i.centerZ()+dz;if(!f.lava(x,z))continue;
var at=new BlockPos(x,EastVolcano223.LAVA,z);level.getChunkAt(at);
require(level.getBlockState(at).is(Blocks.LAVA),"Native crater surface missing lava "+at);lava++;
var c=f.column(x,z);require(level.getBlockState(at.atY(c.top())).isSolidRender(),"Crater bottom breached");
for(int y=c.top()+1;y<=EastVolcano223.LAVA;y++)for(var dir:Direction.Plane.HORIZONTAL){
var q=at.relative(dir).atY(y);var state=level.getBlockState(q);require(state.is(Blocks.LAVA)||state.isSolidRender(),"Native lava wall open "+q);wallChecks++;
}
}
require(lava>350,"Crater too small");
int logs=0,bamboo=0,leaves=0,tallest=0,elevatedSoilRoots=0;var sampled=new HashSet<Long>();var habitats=new HashMap<String,Integer>();
for(String biome:List.of("east_giant_jungle"+i.generation(),"east_bamboo"+i.generation(),"east_sparse_jungle"+i.generation())){
int count=0;
for(int n=0;n<96&&count<8;n++){
double angle=n*Math.PI/48;int x=i.centerX()+(int)(Math.cos(angle)*310),z=i.centerZ()+(int)(Math.sin(angle)*310);
if(!f.biome(x,z).equals(biome))continue;
var cp=new net.minecraft.world.level.ChunkPos(x>>4,z>>4);if(!sampled.add(cp.pack()))continue;
var chunk=level.getChunk(cp.x(),cp.z());count++;habitats.merge(biome,1,Integer::sum);
for(int wx=cp.getMinBlockX();wx<=cp.getMaxBlockX();wx++)for(int wz=cp.getMinBlockZ();wz<=cp.getMaxBlockZ();wz++){
int ground=f.top(wx,wz),streak=0;
for(int y=ground+1;y<=ground+65;y++){
var state=chunk.getBlockState(pos.set(wx,y,wz));
if(y>ground+5&&state.is(net.minecraft.tags.BlockTags.DIRT)&&chunk.getBlockState(pos.above()).is(Blocks.JUNGLE_LOG)&&chunk.getBlockState(pos.above(2)).is(Blocks.JUNGLE_LOG))elevatedSoilRoots++;
if(state.is(Blocks.JUNGLE_LOG)){logs++;streak++;tallest=Math.max(tallest,streak);}else streak=0;
if(state.is(Blocks.BAMBOO))bamboo++;
if(state.is(Blocks.JUNGLE_LEAVES))leaves++;
}
}
}
require(count>=4,"Missing actual habitat samples "+biome);
}
require(logs>200&&leaves>1000&&bamboo>100&&tallest>=30,"Native vegetation missing: logs="+logs+" leaves="+leaves+" bamboo="+bamboo+" height="+tallest);
if(i.generation()==224)require(elevatedSoilRoots==0,"Trees planted on canopy: "+elevatedSoilRoots);
r.result().put("volcano",Map.of("lavaLevel",EastVolcano223.LAVA,"lavaColumnsChecked",lava,"wallChecks",wallChecks));
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"lootChests",chests,"dispensers",traps,"masonry",masonry));
r.result().put("vegetation",Map.of("chunks",sampled.size(),"habitats",habitats,"jungleLogs",logs,"jungleLeaves",leaves,"bamboo",bamboo,"tallestTrunk",tallest,"elevatedSoilRoots",elevatedSoilRoots));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(-32,115,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("crater",new BlockPos(i.centerX(),EastVolcano223.LAVA,i.centerZ()).toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("EAST223_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -134,7 +134,7 @@ final class EcologySurvey176 {
try{Files.writeString(p.out().resolve("survey.json"),JSON.toJson(p.data())+"\n");}catch(java.io.IOException e){throw new IllegalStateException(e);}
p.result().put("survey",Map.of("path",p.out().toString(),"fluidTicks",240,"ponds",p.ponds().size()));
p.result().put("readyForVisitMs",(System.nanoTime()-p.started())/1_000_000);
PENDING.remove(server);WorldgenLab.writeReport(p.directory(),p.result());
PENDING.remove(server);WorldgenDiagnostics.writeReport(p.directory(),p.result());
}
private static Set<BlockPos> water(ServerLevel level,EcologyWater176.Pond p){
var sources=new HashSet<BlockPos>();int retained=0;
@@ -77,5 +77,5 @@ final class Expansion199Checks {
}
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("EXPANSION199_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenLab.writeReport(r.output(),r.result());}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
}
@@ -1,101 +0,0 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.ExpansionAnchorBlock;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.minecraft.core.*;
import net.minecraft.network.chat.*;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.*;
/** Adapter: the accepted laboratory island plus the existing expansion journal, delegates and queue. */
public final class LabExpansions199 extends ExpansionChunkGenerator {
private static final Map<ChunkGenerator,LabExpansions199> HOSTS=new ConcurrentHashMap<>();
private volatile Map<Long,ExpansionIsland> regions=Map.of();
private final Map<ExpansionIsland,BlockPos> relays=new ConcurrentHashMap<>();
private final Map<ExpansionIsland,NorthVillage200.Plan> villages=new ConcurrentHashMap<>();
public NorthVillage200.Plan village(ExpansionIsland island){return villages.computeIfAbsent(island.ready(),NorthVillage200::plan);}
private ServerLevel level;
private final int revision;
private LabExpansions199(ExpansionBiomeSource source,int diameter,int revision){super(source,diameter,true);this.revision=revision;}
public static void register(){ServerLevelEvents.UNLOAD.register((server,level)->HOSTS.remove(level.getChunkSource().getGenerator()));RelayOffers199.register();}
public static void attach(ServerLevel level,NoiseBasedChunkGenerator host){
var source=(AdventureBiomes180)host.getBiomeSource();
var adapter=new LabExpansions199(new ExpansionBiomeSource(source.collectPossibleBiomes().toList()),SharedIsland196.size(host).diameter,SharedIsland196.revision(host));
ExpansionRuntime.attach(level,adapter);source.expansions=adapter;HOSTS.put(host,adapter);
}
public static LabExpansions199 get(ChunkGenerator generator){return HOSTS.get(generator);}
public static boolean owns(ChunkGenerator generator,ChunkPos pos){var adapter=get(generator);return adapter!=null&&adapter.regionAt(pos.getMiddleBlockX(),pos.getMiddleBlockZ())!=null;}
public ExpansionIsland regionAt(int x,int z){return regions.get(ChunkPos.pack(x>>4,z>>4));}
@Override public int rootGeneration(){return revision;}
@Override public String journalDirectory(){return "sanctuary-lab-expansions"+revision;}
@Override public boolean reusesCertifiedVoid(){return false;}
@Override public void initialize(ServerLevel level,List<ExpansionIsland> islands){this.level=level;super.initialize(level,islands);setIslands(islands);}
@Override public void setIslands(List<ExpansionIsland> islands){
super.setIslands(islands);var next=new HashMap<Long,ExpansionIsland>();
for(var island:islands)if(!island.origin()){
// Compile once before exposing the immutable snapshot to native generation workers.
context(island);
int radius=island.reservedRadius();
for(int x=(island.centerX()-radius)>>4;x<=(island.centerX()+radius)>>4;x++)
for(int z=(island.centerZ()-radius)>>4;z<=(island.centerZ()+radius)>>4;z++)if(island.ownsChunk(x,z))next.put(ChunkPos.pack(x,z),island);
}
regions=Map.copyOf(next);
}
public BlockPos relay(ExpansionIsland island){if(NorthTerrain.active(island))return village(island).relay();return relays.computeIfAbsent(island.ready(),i->{
var delegate=delegate(i);var random=randomState(i);
for(int radius=0;radius<=48;radius+=4)for(int x=-radius;x<=radius;x+=4)for(int z=-radius;z<=radius;z+=4){
if(Math.max(Math.abs(x),Math.abs(z))!=radius)continue;
int wx=i.centerX()+x,wz=i.centerZ()+z;
int y=delegate.getBaseHeight(wx,wz,Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,level,random);
if(y>100&&y<320)return new BlockPos(wx,y,wz);
}
throw new IllegalStateException("No relay ground in "+i.id());
});}
@Override public void spawnOriginalMobs(net.minecraft.server.level.WorldGenRegion region){
var chunk=region.getCenter();var island=regionAt(chunk.getMiddleBlockX(),chunk.getMiddleBlockZ());
if(!NorthTerrain.active(island)){super.spawnOriginalMobs(region);return;}
int x=chunk.getMiddleBlockX(),z=chunk.getMiddleBlockZ();
int y=region.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,x,z);
if(y<=0)return;
// Vanilla samples the dimension ceiling (1600), where this floating world's biome is void.
// Use the actual surface for the native biome attributes and retain vanilla spawn rules.
var random=new WorldgenRandom(new LegacyRandomSource(0));
random.setDecorationSeed(island.seed(),chunk.getMinBlockX(),chunk.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(region,new BlockPos(x,y,z),chunk,random);
}
@Override public List<ChunkPos> preparationChunks(ExpansionIsland island){
if(!NorthTerrain.active(island))return List.of();
var plan=village(island);var chunks=new LinkedHashSet<ChunkPos>();
for(var site:List.of(plan.arrival(),plan.relay())){
int radius=site.equals(plan.arrival())?3:1;
for(int x=-radius;x<=radius;x++)for(int z=-radius;z<=radius;z++)chunks.add(new ChunkPos((site.getX()>>4)+x,(site.getZ()>>4)+z));
}
return List.copyOf(chunks);
}
@Override public void finishExpansion(ServerLevel level,ExpansionIsland island){
var pos=relay(island);
// A small marker belongs to this new reserved region; no work is performed on the parent.
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++){
var floor=pos.offset(x,-1,z);level.setBlock(floor,Blocks.TUFF_BRICKS.defaultBlockState(),Block.UPDATE_ALL);
for(int y=0;y<=2;y++)level.setBlock(pos.offset(x,y,z),Blocks.AIR.defaultBlockState(),Block.UPDATE_ALL);
}
int sector=ExpansionIsland.DIRECTIONS.indexOf(island.direction());
level.setBlock(pos,ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,sector),Block.UPDATE_ALL);
}
@Override public void announceExpansion(ServerLevel level,ExpansionIsland island){
var pos=relay(island);
var readable=Component.translatable(NorthTerrain.active(island)?"sanctuary_test.expansion200.ready":"sanctuary_test.expansion199.ready",pos.getX(),pos.getY(),pos.getZ());
var signal=Component.translatable(NorthTerrain.active(island)?"sanctuary_test.expansion200.signal":"sanctuary_test.expansion199.signal").withStyle(style->style
.withFont(new FontDescription.Resource(Identifier.withDefaultNamespace("alt")))
.withColor(0x8ce6d1).withHoverEvent(new HoverEvent.ShowText(readable)));
level.getServer().getPlayerList().broadcastSystemMessage(signal,false);
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("EXPANSION199_READY id={} relay={}",island.id(),pos.toShortString());
}
}
@@ -20,7 +20,7 @@ final class LabVisit200 {
try{
var manifest=Path.of(argument).toAbsolutePath().normalize();var run=manifest.getParent();
var data=JsonParser.parseString(Files.readString(manifest)).getAsJsonObject();var world=data.get("world").getAsString();var revision=Integer.parseInt(world.split("-")[2]);
if(!world.matches("Sanctuary-North-20[0123]-[0-9-]+")||!run.toString().contains("/build/worldgen-lab/")||!data.get("freshVisit").getAsBoolean())throw new IllegalArgumentException("Expected disposable North visit");
if(!world.matches("Sanctuary-(North-(20[012345678]|215)|Diagonal-(225|226|227|228|229|230|231)|East-(223|224)|West-(217|218|219|220|221)|South-(209|210|211|212|213|214|215|216))-[0-9-]+")||!run.toString().contains("/build/worldgen-lab/")||!data.get("freshVisit").getAsBoolean())throw new IllegalArgumentException("Expected disposable North visit");
ScreenEvents.AFTER_INIT.register((mc,screen,w,h)->{
if(!mc.gameDirectory.toPath().toAbsolutePath().normalize().equals(run.resolve("client")))throw new IllegalStateException("Visit directory mismatch");
if(!confirmed&&screen instanceof BackupConfirmScreen&&screen.getTitle().getString().equals(Component.translatable("selectWorld.backupQuestion.experimental").getString())){
@@ -42,7 +42,22 @@ final class LabVisit200 {
var server=mc.getSingleplayerServer();
if(!server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().equals(run.resolve("client/saves").resolve(world)))throw new IllegalStateException("Wrong visit world");
if(SharedIsland196.revision(server.overworld().getChunkSource().getGenerator())!=revision)throw new IllegalStateException("Wrong visit revision");
ready=true;SanctuaryMod.LOGGER.info("NORTH{}_VISIT_OPEN world={} player={}",revision,world,mc.player.blockPosition());
if(revision==221){
var state=fr.koka.sanctuary.building.YellowStone.BLOCK.defaultBlockState();
var tints=mc.getBlockColors().getTintSources(state);
if(tints.size()!=1||tints.getFirst().colorInWorld(state,mc.level,mc.player.blockPosition())!=fr.koka.sanctuary.building.YellowStone.TINT)throw new IllegalStateException("Yellow stone tint missing");
var parts=new java.util.ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(42),parts);
int faces=0;
for(var part:parts)for(var direction:net.minecraft.core.Direction.values())for(var quad:part.getQuads(direction)){
var material=quad.materialInfo();
if(material.tintIndex()!=0||!material.sprite().contents().name().toString().equals("minecraft:block/stone"))throw new IllegalStateException("Yellow stone model/texture missing");
faces++;
}
if(faces!=6)throw new IllegalStateException("Yellow stone model is incomplete");
SanctuaryMod.LOGGER.info("YELLOW_STONE221_CLIENT_PASS faces={} tint={}",faces,Integer.toHexString(fr.koka.sanctuary.building.YellowStone.TINT));
}
ready=true;SanctuaryMod.LOGGER.info("{}{}_VISIT_OPEN world={} player={}",world.split("-")[1].toUpperCase(java.util.Locale.ROOT),revision,world,mc.player.blockPosition());
});
}catch(Exception e){throw new IllegalStateException("Invalid North visit manifest",e);}
}
@@ -18,7 +18,7 @@ final class North200Checks {
static void register(){ServerTickEvents.END_SERVER_TICK.register(North200Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
int revision=Boolean.getBoolean("sanctuary.worldgenLab.north203")?203:Boolean.getBoolean("sanctuary.worldgenLab.north202")?202:Boolean.getBoolean("sanctuary.worldgenLab.north201")?201:200;
int revision=Boolean.getBoolean("sanctuary.worldgenLab.north208")?208:Boolean.getBoolean("sanctuary.worldgenLab.north207")?207:Boolean.getBoolean("sanctuary.worldgenLab.north206")?206:Boolean.getBoolean("sanctuary.worldgenLab.north205")?205:Boolean.getBoolean("sanctuary.worldgenLab.north204")?204:Boolean.getBoolean("sanctuary.worldgenLab.north203")?203:Boolean.getBoolean("sanctuary.worldgenLab.north202")?202:Boolean.getBoolean("sanctuary.worldgenLab.north201")?201:200;
require(adapter!=null&&adapter.rootGeneration()==revision,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","north"+revision);
if(fresh){
@@ -71,6 +71,11 @@ final class North200Checks {
}
require(beds==10&&cellars==1,"Village incomplete: beds="+beds+" cellars="+cellars);
r.result().put("village",Map.of("arrival",village.arrival().toShortString(),"relay",relay.toShortString(),"igloos",village.houses().size(),"cellars",cellars));
if(r.revision()==208){North208Checks.verify(level,island,village,r.result());finish(r);return;}
if(r.revision()==207){North207Checks.verify(level,island,village,r.result());finish(r);return;}
if(r.revision()==206){North206Checks.verify(level,island,village,r.result());finish(r);return;}
if(r.revision()==205){North205Checks.verify(level,island,village,r.result());finish(r);return;}
if(r.revision()==204){North204Checks.verify(level,island,village,r.result());finish(r);return;}
var f=NorthTerrain.field(island);var lake=f.lakes().getFirst();int reach=(int)(lake.radius()*1.75)+3;
for(int x=(lake.x()-reach)>>4;x<=(lake.x()+reach)>>4;x++)for(int z=(lake.z()-reach)>>4;z<=(lake.z()+reach)>>4;z++)level.getChunk(x,z);
int ice=0,water=0;
@@ -100,6 +105,6 @@ final class North200Checks {
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("NORTH200_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenLab.writeReport(r.output(),r.result());}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,56 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
/** Native checks of the real Peaks volume and retained northern ecology, on new development worlds. */
final class North204Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain204.field(island);
double lo=999,hi=0,thickness=0;var covers=new EnumMap<NorthTerrain204.Surface,Integer>(NorthTerrain204.Surface.class);
BlockPos cave=null;double deepest=-1;
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.column(x,z);if(c.top()<0||c.edge()<80)continue;
lo=Math.min(lo,c.top());hi=Math.max(hi,c.top());thickness=Math.max(thickness,c.top()-c.bottom());
var cover=f.cover(x,z);covers.merge(cover.surface(),1,Integer::sum);
double room=cover.caveHigh()-cover.caveLow();if(room>deepest){deepest=room;cave=new BlockPos(x,(int)c.top(),z);}
}
require(hi-lo>60&&thickness>100,"Peaks collapsed into a sheet: "+lo+".."+hi+", thickness="+thickness);
result.put("peaksRelief",Map.of("minTop",lo,"maxTop",hi,"maxThickness",thickness,"surfaces",covers));
var wood=f.sample(NorthTerrain204.Surface.OLD_GROWTH);int logs=0,leaves=0,tallest=0,podzol=0;
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);int trunk=0;
for(int y=Math.max(1,(int)c.top()-20);y<=c.top()+80;y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));
if(b.is(Blocks.SPRUCE_LOG)){logs++;trunk++;}if(b.is(Blocks.SPRUCE_LEAVES))leaves++;if(b.is(Blocks.PODZOL))podzol++;
}
tallest=Math.max(tallest,trunk);
}
require(tallest>=40&&logs>100&&leaves>100&&podzol>50,"Giant taiga absent: "+tallest+" / "+logs+" / "+podzol);
result.put("nativeGiantTaiga",Map.of("centre",wood.toShortString(),"tallestTrunk",tallest,"logs",logs,"leaves",leaves,"podzol",podzol));
int ice=0,air=0;
require(cave!=null&&deepest>20,"No covered cave volume");
for(int dx=-24;dx<=24;dx+=4)for(int dz=-24;dz<=24;dz+=4){
int x=cave.getX()+dx,z=cave.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.cover(x,z);
for(int y=c.caveLow();y<=c.caveHigh();y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));if(b.is(Blocks.PACKED_ICE)||b.is(Blocks.BLUE_ICE))ice++;if(b.isAir())air++;
}
}
require(ice>20&&air>30,"Native ice cavern absent: "+ice+" / "+air);
result.put("iceCavern",Map.of("peak",cave.toShortString(),"ice",ice,"air",air));
var peaks=f.sample(NorthTerrain204.Surface.GLACIER);var materials=new HashMap<String,Integer>();
for(int dx=-24;dx<=24;dx+=2)for(int dz=-24;dz<=24;dz+=2){
int x=peaks.getX()+dx,z=peaks.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);
int y=chunk.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR,x&15,z&15);
for(int k=y-6;k<=y;k++){
var b=chunk.getBlockState(new BlockPos(x,k,z)).getBlock();materials.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).toString(),1,Integer::sum);
}
}
require(materials.containsKey("minecraft:packed_ice")||materials.containsKey("minecraft:blue_ice"),"No glacier");
result.put("glacier",Map.of("centre",peaks.toShortString(),"materials",materials));
}
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
}
@@ -0,0 +1,82 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
/** Native checks of the unwarped noise volume and retained northern ecology, on new development worlds. */
final class North205Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain205.field(island);
double lo=999,hi=0,thickness=0;var covers=new EnumMap<NorthTerrain205.Surface,Integer>(NorthTerrain205.Surface.class);
BlockPos cave=null;double deepest=-1;
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.column(x,z);if(c.top()<0||c.edge()<80)continue;
lo=Math.min(lo,c.top());hi=Math.max(hi,c.top());thickness=Math.max(thickness,c.top()-c.bottom());
var cover=f.cover(x,z);covers.merge(cover.surface(),1,Integer::sum);
// Raw 3D noise does not guarantee a cave in the thickest mountain column.
// Locate a genuinely empty covered pocket, then verify its blocks in native chunks.
int open=0;for(int y=cover.caveLow()+4;y<cover.caveHigh()-4;y+=4)if(f.density(x,y,z)<-.05)open++;
double room=cover.caveHigh()-cover.caveLow();double score=open*100+room*.01;
if(open>8&&room>20&&score>deepest){deepest=score;cave=new BlockPos(x,(int)c.top(),z);}
}
require(hi-lo>60&&thickness>100,"Noise collapsed into a sheet: "+lo+".."+hi+", thickness="+thickness);
result.put("noiseRelief",Map.of("minTop",lo,"maxTop",hi,"maxThickness",thickness,"surfaces",covers));
var wood=f.sample(NorthTerrain205.Surface.OLD_GROWTH);int logs=0,leaves=0,tallest=0,podzol=0;
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);int trunk=0;
for(int y=Math.max(1,(int)c.top()-20);y<=c.top()+80;y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));
if(b.is(Blocks.SPRUCE_LOG)){logs++;trunk++;}if(b.is(Blocks.SPRUCE_LEAVES))leaves++;if(b.is(Blocks.PODZOL))podzol++;
}
tallest=Math.max(tallest,trunk);
}
require(tallest>=40&&logs>100&&leaves>100&&podzol>50,"Giant taiga absent: "+tallest+" / "+logs+" / "+podzol);
result.put("nativeGiantTaiga",Map.of("centre",wood.toShortString(),"tallestTrunk",tallest,"logs",logs,"leaves",leaves,"podzol",podzol));
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("NORTH205_GEOGRAPHY {} / {}",result.get("noiseRelief"),result.get("nativeGiantTaiga"));
int ice=0,air=0;
require(cave!=null&&deepest>20,"No covered cave volume");
for(int dx=-24;dx<=24;dx+=4)for(int dz=-24;dz<=24;dz+=4){
int x=cave.getX()+dx,z=cave.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.cover(x,z);
for(int y=c.caveLow();y<=c.caveHigh();y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));if(b.is(Blocks.PACKED_ICE)||b.is(Blocks.BLUE_ICE))ice++;if(b.isAir())air++;
}
}
require(ice>20&&air>30,"Native ice cavern absent: "+ice+" / "+air);
result.put("iceCavern",Map.of("peak",cave.toShortString(),"ice",ice,"air",air));
var peaks=f.sample(NorthTerrain205.Surface.GLACIER);var materials=new HashMap<String,Integer>();
for(int dx=-24;dx<=24;dx+=2)for(int dz=-24;dz<=24;dz+=2){
int x=peaks.getX()+dx,z=peaks.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);
int y=chunk.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR,x&15,z&15);
for(int k=y-6;k<=y;k++){
var b=chunk.getBlockState(new BlockPos(x,k,z)).getBlock();materials.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).toString(),1,Integer::sum);
}
}
result.put("glacier",Map.of("centre",peaks.toShortString(),"materials",materials));
// The lower, unwarped relief also uses normal ice; a glacier need not be blue ice.
int glacierIce=materials.getOrDefault("minecraft:ice",0)+materials.getOrDefault("minecraft:packed_ice",0)+materials.getOrDefault("minecraft:blue_ice",0);
require(glacierIce>50,"No native glacier ice: "+materials);
// Compare stored biome cells on both sides of real chunk edges with the same world field.
// A resolver snapshot or request-order error must not leave one chunk on a different palette.
int checked=0,mismatches=0;
for(var centre:List.of(wood,peaks,village.arrival())){
int cx=centre.getX()>>4,cz=centre.getZ()>>4;
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
var chunk=level.getChunk(cx+dx,cz+dz);
for(int qx=(cx+dx)*4;qx<(cx+dx)*4+4;qx++)for(int qz=(cz+dz)*4;qz<(cz+dz)*4+4;qz++){
int top=(int)f.column(qx*4,qz*4).top();if(top<0)continue;
for(int qy=(top-20)>>2;qy<=top/4+2;qy++){
String expected=f.biome(qx*4,qy*4,qz*4);
String actual=chunk.getNoiseBiome(qx,qy,qz).getRegisteredName();
checked++;if(!actual.equals(expected))mismatches++;
}
}
}
}
result.put("storedBiomes",Map.of("checked",checked,"mismatches",mismatches));
require(mismatches==0,"Stored chunk biomes disagree with global ecology: "+mismatches+"/"+checked);
}
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
}
@@ -0,0 +1,123 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
/** Native checks of the unwarped noise volume and retained northern ecology, on new development worlds. */
final class North206Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain206.field(island);
double lo=999,hi=0,thickness=0;var covers=new EnumMap<NorthTerrain206.Surface,Integer>(NorthTerrain206.Surface.class);
BlockPos cave=null,fault=null;double deepest=-1,bestFault=-1;
var original=new NorthTerrain205.Field(island,f.shelf.original);long oldRock=0,newRock=0;int surfaceChanged=0;
for(int dx=-384;dx<=384;dx+=24)for(int dz=-384;dz<=384;dz+=24){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.column(x,z);if(c.top()<0||c.edge()<80)continue;
if(c.top()!=original.column(x,z).top())surfaceChanged++;
for(int y=4;y<316;y+=4){if(original.density(x,y,z)>0)oldRock++;if(f.density(x,y,z)>0)newRock++;}
int lining=0;for(int y=(int)c.bottom()+8;y<c.top()-24;y++)if(f.fault(x,y,z)>=0&&f.fault(x,y,z)<1.8&&f.density(x,y,z)>0)lining++;
if(lining>bestFault){bestFault=lining;fault=new BlockPos(x,(int)c.top()-32,z);}
lo=Math.min(lo,c.top());hi=Math.max(hi,c.top());thickness=Math.max(thickness,c.top()-c.bottom());
var cover=f.cover(x,z);covers.merge(cover.surface(),1,Integer::sum);
// Raw 3D noise does not guarantee a cave in the thickest mountain column.
// Locate a genuinely empty covered pocket, then verify its blocks in native chunks.
int open=0;for(int y=cover.caveLow()+4;y<cover.caveHigh()-4;y+=4)if(f.density(x,y,z)<-.05)open++;
double room=cover.caveHigh()-cover.caveLow();double score=open*100+room*.01;
if(open>2&&room>12&&score>deepest){deepest=score;cave=new BlockPos(x,(int)c.top(),z);}
}
require(hi-lo>60&&thickness<110,"Roof relief or shelf thickness wrong: "+lo+".."+hi+", thickness="+thickness);
require(surfaceChanged==0,"Roof changed in "+surfaceChanged+" columns");
require(newRock<oldRock*.8,"Too much underground mass retained: "+newRock+"/"+oldRock);
result.put("massComparison",Map.of("oldRockSamples",oldRock,"newRockSamples",newRock,"changedSurfaceColumns",surfaceChanged));
result.put("noiseRelief",Map.of("minTop",lo,"maxTop",hi,"maxThickness",thickness,"surfaces",covers));
require(fault!=null&&bestFault>0,"No amethyst seam in density");
BlockPos entrance=null;
int amethyst=0,calcite=0,basalt=0,crystals=0,openAmethyst=0;
for(int dx=-24;dx<=24;dx++)for(int dz=-24;dz<=24;dz++){
int x=fault.getX()+dx,z=fault.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
for(int y=Math.max(1,(int)c.bottom());y<c.top()-20;y++){
var pos=new BlockPos(x,y,z);var b=chunk.getBlockState(pos);
if(b.is(Blocks.AMETHYST_BLOCK)||b.is(Blocks.BUDDING_AMETHYST)){
amethyst++;
for(var dir:net.minecraft.core.Direction.values())if(level.getBlockState(pos.relative(dir)).isAir()){openAmethyst++;if(entrance==null&&level.getBlockState(pos.relative(dir).above()).isAir())entrance=pos.relative(dir);break;}
}
if(b.is(Blocks.CALCITE))calcite++;if(b.is(Blocks.SMOOTH_BASALT))basalt++;
if(b.getBlock() instanceof net.minecraft.world.level.block.AmethystClusterBlock)crystals++;
}
}
require(amethyst>50&&calcite>50&&basalt>50&&openAmethyst>10&&crystals>5,"Open amethyst fault absent: "+amethyst+"/"+openAmethyst+"/"+crystals);
result.put("amethystFault",Map.of("centre",fault.toShortString(),"entrance",entrance==null?fault.toShortString():entrance.toShortString(),"amethyst",amethyst,"calcite",calcite,"basalt",basalt,"crystals",crystals,"openFaces",openAmethyst));
int nativeWater=0;BlockPos poolSite=null;
var checkedPools=new HashSet<BlockPos>();
outer:for(int dx=-256;dx<=256;dx+=64)for(int dz=-256;dz<=256;dz+=64){
var pools=f.poolsNear(level,island.centerX()+dx,island.centerZ()+dz);
for(var p:pools){
if(!checkedPools.add(p))continue;
level.getChunkAt(p);if(!level.getBlockState(p).is(Blocks.WATER))continue;
require(level.getBlockState(p.below()).isSolidRender(),"Pool has no floor: "+p);
for(var n:List.of(p.north(),p.south(),p.east(),p.west()))require(level.getBlockState(n).isSolidRender()||level.getBlockState(n).is(Blocks.WATER),"Pool leaks: "+n);
nativeWater++;poolSite=p;
}
if(nativeWater>=6)break outer;
}
require(nativeWater>=6,"No sealed shallow wet cavity; planned="+checkedPools.size());
result.put("wetCavity",Map.of("water",nativeWater,"site",poolSite.toShortString()));
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("NORTH206_CAVITIES mass={} fault={} water={}",result.get("massComparison"),result.get("amethystFault"),result.get("wetCavity"));
var wood=f.sample(NorthTerrain206.Surface.OLD_GROWTH);int logs=0,leaves=0,tallest=0,podzol=0;
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);int trunk=0;
for(int y=Math.max(1,(int)c.top()-20);y<=c.top()+80;y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));
if(b.is(Blocks.SPRUCE_LOG)){logs++;trunk++;}if(b.is(Blocks.SPRUCE_LEAVES))leaves++;if(b.is(Blocks.PODZOL))podzol++;
}
tallest=Math.max(tallest,trunk);
}
require(tallest>=40&&logs>100&&leaves>100&&podzol>50,"Giant taiga absent: "+tallest+" / "+logs+" / "+podzol);
result.put("nativeGiantTaiga",Map.of("centre",wood.toShortString(),"tallestTrunk",tallest,"logs",logs,"leaves",leaves,"podzol",podzol));
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("NORTH206_GEOGRAPHY {} / {}",result.get("noiseRelief"),result.get("nativeGiantTaiga"));
int ice=0,air=0;
require(cave!=null&&deepest>20,"No covered cave volume");
for(int dx=-24;dx<=24;dx+=4)for(int dz=-24;dz<=24;dz+=4){
int x=cave.getX()+dx,z=cave.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.cover(x,z);
for(int y=c.caveLow();y<=c.caveHigh();y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));if(b.is(Blocks.PACKED_ICE)||b.is(Blocks.BLUE_ICE))ice++;if(b.isAir())air++;
}
}
require(ice>20&&air>30,"Native ice cavern absent: "+ice+" / "+air);
result.put("iceCavern",Map.of("peak",cave.toShortString(),"ice",ice,"air",air));
var peaks=f.sample(NorthTerrain206.Surface.GLACIER);var materials=new HashMap<String,Integer>();
for(int dx=-24;dx<=24;dx+=2)for(int dz=-24;dz<=24;dz+=2){
int x=peaks.getX()+dx,z=peaks.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);
int y=chunk.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR,x&15,z&15);
for(int k=y-6;k<=y;k++){
var b=chunk.getBlockState(new BlockPos(x,k,z)).getBlock();materials.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).toString(),1,Integer::sum);
}
}
result.put("glacier",Map.of("centre",peaks.toShortString(),"materials",materials));
// The lower, unwarped relief also uses normal ice; a glacier need not be blue ice.
int glacierIce=materials.getOrDefault("minecraft:ice",0)+materials.getOrDefault("minecraft:packed_ice",0)+materials.getOrDefault("minecraft:blue_ice",0);
require(glacierIce>50,"No native glacier ice: "+materials);
// Compare stored biome cells on both sides of real chunk edges with the same world field.
// A resolver snapshot or request-order error must not leave one chunk on a different palette.
int checked=0,mismatches=0;
for(var centre:List.of(wood,peaks,village.arrival())){
int cx=centre.getX()>>4,cz=centre.getZ()>>4;
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
var chunk=level.getChunk(cx+dx,cz+dz);
for(int qx=(cx+dx)*4;qx<(cx+dx)*4+4;qx++)for(int qz=(cz+dz)*4;qz<(cz+dz)*4+4;qz++){
int top=(int)f.column(qx*4,qz*4).top();if(top<0)continue;
for(int qy=(top-20)>>2;qy<=top/4+2;qy++){
String expected=f.biome(qx*4,qy*4,qz*4);
String actual=chunk.getNoiseBiome(qx,qy,qz).getRegisteredName();
checked++;if(!actual.equals(expected))mismatches++;
}
}
}
}
result.put("storedBiomes",Map.of("checked",checked,"mismatches",mismatches));
require(mismatches==0,"Stored chunk biomes disagree with global ecology: "+mismatches+"/"+checked);
}
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
}
@@ -0,0 +1,98 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Fresh-world checks of the new landscape and a complete, furnished native mansion. */
final class North207Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain207.field(island);var context=SamplerContext.builder().build();
int raised=0,unchanged=0,deep=0;double highest=0;BlockPos summit=null,fault=null;int liningBest=0;
for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.column(x,z);if(c.top()<0||c.edge()<64)continue;
var old=f.shelf.column(context,x,z);double lift=c.top()-old.top();
if(lift>2)raised++;else unchanged++;
if(lift>highest){highest=lift;summit=new BlockPos(x,(int)c.top()+2,z);}
for(int y=8;y<old.top()-8;y+=8){require(f.density(x,y,z)==f.shelf.sampleValue(context,x,y,z),"Deep density changed");deep++;}
int lining=0;for(int y=(int)c.bottom()+8;y<old.top()-24;y++)if(f.fault(x,y,z)>=0&&f.fault(x,y,z)<1.8&&f.density(x,y,z)>0)lining++;
if(lining>liningBest){liningBest=lining;fault=new BlockPos(x,old.top()-32,z);}
}
require(raised>20&&unchanged>raised*2&&highest>24&&highest<60,"Missing or excessive local relief: "+raised+"/"+unchanged+"/"+highest);
result.put("crowns",Map.of("count",f.relief.crowns.size(),"raisedColumns",raised,"unchangedColumns",unchanged,"maxAddedHeight",highest,"deepSamplesUnchanged",deep,"summit",summit.toShortString()));
int amethyst=0,open=0;
require(fault!=null,"No amethyst field");
for(int dx=-16;dx<=16;dx++)for(int dz=-16;dz<=16;dz++){
int x=fault.getX()+dx,z=fault.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
for(int y=(int)c.bottom();y<f.shelf.column(context,x,z).top()-20;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);
if(b.is(Blocks.AMETHYST_BLOCK)||b.is(Blocks.BUDDING_AMETHYST)){
amethyst++;for(var d:net.minecraft.core.Direction.values())if(level.getBlockState(p.relative(d)).isAir()){open++;break;}
}
}
}
require(amethyst>100&&open>30,"Accepted open amethyst lost");result.put("amethyst",Map.of("site",fault.toShortString(),"blocks",amethyst,"openFaces",open));
var snowSite=forest(f,island,true);var drySite=forest(f,island,false);
result.put("snowyForest",checkForest(level,f,snowSite,true));result.put("dryForest",checkForest(level,f,drySite,false));
var mansion=NorthMansion207.cached(island);require(mansion!=null,"No supported mansion");var box=mansion.bounds();
for(int x=box.minX()>>4;x<=box.maxX()>>4;x++)for(int z=box.minZ()>>4;z<=box.maxZ()>>4;z++)level.getChunk(x,z);
int spruce=0,calcite=0,tuff=0,stairs=0,chests=0,loot=0,oldWood=0,leaves=0;
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 p=new BlockPos(x,y,z);var b=level.getBlockState(p);
if(b.is(Blocks.SPRUCE_PLANKS)||b.is(Blocks.STRIPPED_SPRUCE_LOG))spruce++;
if(b.is(Blocks.CALCITE))calcite++;if(b.is(Blocks.TUFF_BRICKS))tuff++;
if(b.is(Blocks.SPRUCE_STAIRS))stairs++;if(b.is(Blocks.CHEST))chests++;
if(b.is(Blocks.CHEST)&&level.getBlockEntity(p) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)loot++;
if(b.is(Blocks.DARK_OAK_PLANKS)||b.is(Blocks.OAK_PLANKS))oldWood++;
// Native decorative oak leaves are allowed; invading spruce canopies are not.
if(b.is(Blocks.SPRUCE_LEAVES))leaves++;
}
require(spruce>1000&&calcite>200&&tuff>100&&stairs>100&&chests>0&&oldWood==0,"Mansion absent or palette failed: "+spruce+"/"+calcite+"/"+stairs+"/"+chests+" old="+oldWood);
require(mansion.maxFoundationDepth()<=20,"Unsupported mansion");
require(loot>0,"No native mansion loot table");
for(var at:mansion.approach()){
require(level.getBlockState(at).is(Blocks.TUFF_BRICK_STAIRS)&&level.getBlockState(at.below()).isSolidRender(),"Unsupported entrance stair");
require(level.getBlockState(at.above()).isAir()&&level.getBlockState(at.above(2)).isAir(),"Blocked entrance approach");
}
result.put("mansion",Map.of("origin",mansion.origin().toShortString(),"bounds",box.toString(),"pieces",mansion.pieces().size(),"maxFoundationDepth",mansion.maxFoundationDepth(),"spruce",spruce,"calcite",calcite,"tuff",tuff,"stairs",stairs,"chests",chests,"spruceLeavesInBox",leaves));
result.put("mansionAccess",Map.of("stairs",mansion.approach().size(),"nativeLootChests",loot));
int checked=0,mismatches=0;
for(var site:List.of(snowSite,drySite,summit))for(int x=site.getX()-16;x<=site.getX()+16;x+=4)for(int z=site.getZ()-16;z<=site.getZ()+16;z+=4){
int qx=x>>2,qz=z>>2,top=(int)f.column(qx*4,qz*4).top();var chunk=level.getChunk(x>>4,z>>4);
for(int qy=(top-20)>>2;qy<=(top+8)>>2;qy++){checked++;if(!chunk.getNoiseBiome(qx,qy,qz).getRegisteredName().equals(f.biome(qx*4,qy*4,qz*4)))mismatches++;}
}
require(mismatches==0,"Stored ecology mismatch");result.put("storedBiomes",Map.of("checked",checked,"mismatches",mismatches));
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("NORTH207_VERIFIED crowns={} snowy={} dry={} mansion={}",result.get("crowns"),result.get("snowyForest"),result.get("dryForest"),result.get("mansion"));
}
private static BlockPos forest(NorthTerrain207.Field f,ExpansionIsland i,boolean snow){
BlockPos best=null;int bestScore=-1;
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()<80||NorthMansion207.reserved(i,x,z))continue;
int score=0;
for(int a=-24;a<=24;a+=8)for(int b=-24;b<=24;b+=8){var cover=f.cover(x+a,z+b).surface();if((cover==NorthTerrain207.Surface.OLD_GROWTH||cover==NorthTerrain207.Surface.TAIGA)&&f.forestSnow(x+a,z+b)==snow)score++;}
if(score>bestScore){bestScore=score;best=new BlockPos(x,(int)c.top()+2,z);}
}
require(bestScore>30,"Missing coherent forest patch "+snow);return best;
}
private static Map<String,Object> checkForest(ServerLevel level,NorthTerrain207.Field f,BlockPos centre,boolean snowy){
int eligible=0,snow=0,dry=0,logs=0,leaves=0;
for(int dx=-24;dx<=24;dx++)for(int dz=-24;dz<=24;dz++){
int x=centre.getX()+dx,z=centre.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);var kind=f.cover(x,z).surface();
boolean forest=kind==NorthTerrain207.Surface.OLD_GROWTH||kind==NorthTerrain207.Surface.TAIGA;
for(int y=(int)c.top()-12;y<c.top()+80;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);
if(b.is(Blocks.SPRUCE_LOG))logs++;if(b.is(Blocks.SPRUCE_LEAVES))leaves++;
if(forest&&f.forestSnow(x,z)==snowy&&(b.is(Blocks.GRASS_BLOCK)||b.is(Blocks.PODZOL))){
var above=chunk.getBlockState(p.above());eligible++;if(above.is(Blocks.SNOW)){snow++;require(b.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY),"Snowy soil flag absent");}else if(!above.is(Blocks.SPRUCE_LOG))dry++;
}
}
}
require(logs>20&&leaves>100,"Forest missing around "+centre);
require(snowy?snow>100:dry>100&&snow==0,"Snow mix missing: "+snow+"/"+dry);
return Map.of("site",centre.toShortString(),"eligible",eligible,"snow",snow,"dry",dry,"logs",logs,"leaves",leaves);
}
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
}
@@ -0,0 +1,129 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Fresh-world checks of the new landscape and a complete, furnished native mansion. */
final class North208Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain208.field(island);var context=SamplerContext.builder().build();
int raised=0,unchanged=0,deep=0;double highest=0;BlockPos summit=null,fault=null;int liningBest=0;
for(int dx=-384;dx<=384;dx+=16)for(int dz=-384;dz<=384;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.column(x,z);if(c.top()<0||c.edge()<64)continue;
var old=f.shelf.column(context,x,z);double lift=c.top()-old.top();
if(lift>2)raised++;else unchanged++;
if(lift>highest){highest=lift;summit=new BlockPos(x,(int)c.top()+2,z);}
for(int y=8;y<old.top()-8;y+=8){require(f.density(x,y,z)==f.shelf.sampleValue(context,x,y,z),"Deep density changed");deep++;}
int lining=0;for(int y=(int)c.bottom()+8;y<old.top()-24;y++)if(f.fault(x,y,z)>=0&&f.fault(x,y,z)<1.8&&f.density(x,y,z)>0)lining++;
if(lining>liningBest){liningBest=lining;fault=new BlockPos(x,old.top()-32,z);}
}
require(raised>20&&unchanged>raised*2&&highest>24&&highest<60,"Missing or excessive local relief: "+raised+"/"+unchanged+"/"+highest+" at "+summit);
result.put("crowns",Map.of("count",f.relief.crowns.size(),"raisedColumns",raised,"unchangedColumns",unchanged,"maxAddedHeight",highest,"deepSamplesUnchanged",deep,"summit",summit.toShortString()));
int pointed=0;var peakDrops=new ArrayList<Double>();
for(var crown:f.relief.crowns){
int px=crown.x(),pz=crown.z();double top=-1;
for(int x=crown.x()-6;x<=crown.x()+6;x++)for(int z=crown.z()-6;z<=crown.z()+6;z++)if(f.relief.top(x,z)>top){top=f.relief.top(x,z);px=x;pz=z;}
double fall=0;
for(int n=0;n<8;n++){double a=n*Math.PI/4;fall+=top-f.relief.top(px+(int)Math.round(8*Math.cos(a)),pz+(int)Math.round(8*Math.sin(a)));}
peakDrops.add(fall/8);if(fall/8>3)pointed++;
}
require(pointed>=3,"Native summits still rounded: "+peakDrops);result.put("summitDropsAt8Blocks",peakDrops);
int ice=0,solidBeds=0,formations=0;var sites=new ArrayList<Map<String,Object>>();
for(var t:f.relief.terraces){
int localIce=0;
for(int x=t.x()-6;x<=t.x()+6;x++)for(int z=t.z()-6;z<=t.z()+6;z++){
var p=f.relief.pocket(x,z);if(p==null)continue;
var chunk=level.getChunk(x>>4,z>>4);
for(int y=p.level()-4;y<=p.level()+2;y++){
var at=new BlockPos(x,y,z);var b=chunk.getBlockState(at);
if(b.is(Blocks.ICE)||b.is(Blocks.PACKED_ICE)||b.is(Blocks.BLUE_ICE)){
localIce++;ice++;if(level.getBlockState(at.below()).isSolidRender())solidBeds++;
}
}
}
sites.add(Map.of("x",t.x(),"y",t.level(),"z",t.z(),"iceBlocks",localIce));
}
for(var sp:f.relief.spires)for(int x=sp.x()-3;x<=sp.x()+3;x++)for(int z=sp.z()-3;z<=sp.z()+3;z++){
var chunk=level.getChunk(x>>4,z>>4);
for(int y=sp.base()+2;y<=sp.base()+sp.height();y++)if(chunk.getBlockState(new BlockPos(x,y,z)).is(Blocks.PACKED_ICE))formations++;
}
require(f.relief.terraces.size()>=2&&ice>40&&solidBeds>ice*.90,"Missing or unsupported glacial benches: "+ice+"/"+solidBeds);
require(formations>10,"Missing bank ice formations: "+formations);
result.put("glacialBenches",Map.of("sites",sites,"ice",ice,"supported",solidBeds,"formationIce",formations));
int amethyst=0,open=0;
require(fault!=null,"No amethyst field");
for(int dx=-16;dx<=16;dx++)for(int dz=-16;dz<=16;dz++){
int x=fault.getX()+dx,z=fault.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
for(int y=(int)c.bottom();y<f.shelf.column(context,x,z).top()-20;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);
if(b.is(Blocks.AMETHYST_BLOCK)||b.is(Blocks.BUDDING_AMETHYST)){
amethyst++;for(var d:net.minecraft.core.Direction.values())if(level.getBlockState(p.relative(d)).isAir()){open++;break;}
}
}
}
require(amethyst>100&&open>30,"Accepted open amethyst lost");result.put("amethyst",Map.of("site",fault.toShortString(),"blocks",amethyst,"openFaces",open));
var snowSite=forest(f,island,true);var drySite=forest(f,island,false);
result.put("snowyForest",checkForest(level,f,snowSite,true));result.put("dryForest",checkForest(level,f,drySite,false));
var mansion=NorthMansion208.cached(island);require(mansion!=null,"No supported mansion");var box=mansion.bounds();
for(int x=box.minX()>>4;x<=box.maxX()>>4;x++)for(int z=box.minZ()>>4;z<=box.maxZ()>>4;z++)level.getChunk(x,z);
int spruce=0,calcite=0,tuff=0,stairs=0,chests=0,loot=0,oldWood=0,leaves=0;
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 p=new BlockPos(x,y,z);var b=level.getBlockState(p);
if(b.is(Blocks.SPRUCE_PLANKS)||b.is(Blocks.STRIPPED_SPRUCE_LOG))spruce++;
if(b.is(Blocks.CALCITE))calcite++;if(b.is(Blocks.TUFF_BRICKS))tuff++;
if(b.is(Blocks.SPRUCE_STAIRS))stairs++;if(b.is(Blocks.CHEST))chests++;
if(b.is(Blocks.CHEST)&&level.getBlockEntity(p) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)loot++;
if(b.is(Blocks.DARK_OAK_PLANKS)||b.is(Blocks.OAK_PLANKS))oldWood++;
// Native decorative oak leaves are allowed; invading spruce canopies are not.
if(b.is(Blocks.SPRUCE_LEAVES))leaves++;
}
require(spruce>1000&&calcite>200&&tuff>100&&stairs>100&&chests>0&&oldWood==0,"Mansion absent or palette failed: "+spruce+"/"+calcite+"/"+stairs+"/"+chests+" old="+oldWood);
require(mansion.maxFoundationDepth()<=20,"Unsupported mansion");
require(loot>0,"No native mansion loot table");
for(var at:mansion.approach()){
require(level.getBlockState(at).is(Blocks.TUFF_BRICK_STAIRS)&&level.getBlockState(at.below()).isSolidRender(),"Unsupported entrance stair");
require(level.getBlockState(at.above()).isAir()&&level.getBlockState(at.above(2)).isAir(),"Blocked entrance approach");
}
result.put("mansion",Map.of("origin",mansion.origin().toShortString(),"bounds",box.toString(),"pieces",mansion.pieces().size(),"maxFoundationDepth",mansion.maxFoundationDepth(),"spruce",spruce,"calcite",calcite,"tuff",tuff,"stairs",stairs,"chests",chests,"spruceLeavesInBox",leaves));
result.put("mansionAccess",Map.of("stairs",mansion.approach().size(),"nativeLootChests",loot));
int checked=0,mismatches=0;
for(var site:List.of(snowSite,drySite,summit))for(int x=site.getX()-16;x<=site.getX()+16;x+=4)for(int z=site.getZ()-16;z<=site.getZ()+16;z+=4){
int qx=x>>2,qz=z>>2,top=(int)f.column(qx*4,qz*4).top();var chunk=level.getChunk(x>>4,z>>4);
for(int qy=(top-20)>>2;qy<=(top+8)>>2;qy++){checked++;if(!chunk.getNoiseBiome(qx,qy,qz).getRegisteredName().equals(f.biome(qx*4,qy*4,qz*4)))mismatches++;}
}
require(mismatches==0,"Stored ecology mismatch");result.put("storedBiomes",Map.of("checked",checked,"mismatches",mismatches));
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("NORTH208_VERIFIED crowns={} snowy={} dry={} mansion={}",result.get("crowns"),result.get("snowyForest"),result.get("dryForest"),result.get("mansion"));
}
private static BlockPos forest(NorthTerrain208.Field f,ExpansionIsland i,boolean snow){
BlockPos best=null;int bestScore=-1;
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=i.centerX()+dx,z=i.centerZ()+dz;var c=f.column(x,z);if(c.edge()<80||NorthMansion208.reserved(i,x,z))continue;
int score=0;
for(int a=-24;a<=24;a+=8)for(int b=-24;b<=24;b+=8){var cover=f.cover(x+a,z+b).surface();if((cover==NorthTerrain208.Surface.OLD_GROWTH||cover==NorthTerrain208.Surface.TAIGA)&&f.forestSnow(x+a,z+b)==snow)score++;}
if(score>bestScore){bestScore=score;best=new BlockPos(x,(int)c.top()+2,z);}
}
require(bestScore>30,"Missing coherent forest patch "+snow);return best;
}
private static Map<String,Object> checkForest(ServerLevel level,NorthTerrain208.Field f,BlockPos centre,boolean snowy){
int eligible=0,snow=0,dry=0,logs=0,leaves=0;
for(int dx=-24;dx<=24;dx++)for(int dz=-24;dz<=24;dz++){
int x=centre.getX()+dx,z=centre.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);var kind=f.cover(x,z).surface();
boolean forest=kind==NorthTerrain208.Surface.OLD_GROWTH||kind==NorthTerrain208.Surface.TAIGA;
for(int y=(int)c.top()-12;y<c.top()+80;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);
if(b.is(Blocks.SPRUCE_LOG))logs++;if(b.is(Blocks.SPRUCE_LEAVES))leaves++;
if(forest&&f.forestSnow(x,z)==snowy&&(b.is(Blocks.GRASS_BLOCK)||b.is(Blocks.PODZOL))){
var above=chunk.getBlockState(p.above());eligible++;if(above.is(Blocks.SNOW)){snow++;require(b.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.SNOWY),"Snowy soil flag absent");}else if(!above.is(Blocks.SPRUCE_LOG))dry++;
}
}
}
require(logs>20&&leaves>100,"Forest missing around "+centre);
require(snowy?snow>100:dry>100&&snow==0,"Snow mix missing: "+snow+"/"+dry);
return Map.of("site",centre.toShortString(),"eligible",eligible,"snow",snow,"dry",dry,"logs",logs,"leaves",leaves);
}
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
}
@@ -11,7 +11,6 @@ import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
public final class QuickTestClient implements ClientModInitializer {
private final Set<Screen> initialized=Collections.newSetFromMap(new WeakHashMap<>());
public void onInitializeClient(){
SharedIsland196Screen.register();
AscentView184.register();
PhotoVisit193.register();
LabVisit200.register();
@@ -19,11 +19,7 @@ public final class QuickTestMod implements ModInitializer {
DuoLab.register();
HordeLab.register();
PalaceLab.register();
AnchorOffers186.register();
AnchorOffers187.register();
AnchorOffers188.register();
AnchorOffers189.register();
WorldgenLab.register();
WorldgenDiagnostics.register();
PayloadTypeRegistry.clientboundConfiguration().register(Mode.TYPE,Mode.CODEC);
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server)->{
if(ServerConfigurationNetworking.canSend(handler,Mode.TYPE))
@@ -0,0 +1,108 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Disposable-world activation, interrupted preparation and native regional structure checks. */
final class Relays215Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,boolean south,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(Relays215Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
boolean current=Boolean.getBoolean("sanctuary.worldgenLab.south216");
boolean south=current||Boolean.getBoolean("sanctuary.worldgenLab.south215");int sector=south?4:0;
require(adapter!=null&&adapter.rootGeneration()==(current?216:215),"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope",current?"south216":south?"south215":"north215");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==sector).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing source anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(sector);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,south,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,south,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(RegionalRelay215.active(admitted)&&admitted.diameter()==1024,"Wrong region");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var i=session.island(admitted.id());
if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var p=RegionalRelay215.plan(i);
require(adapter.relay(i).equals(p.anchor()),"Offering still uses old ground marker");
require(adapter.preparationChunks(i).size()<=110,"Unbounded preparation");
var touched=new HashSet<Long>();int blocks=0;
for(var part:p.chunks().entrySet()){
var cp=net.minecraft.world.level.ChunkPos.unpack(part.getKey());var chunk=level.getChunk(cp.x(),cp.z());touched.add(part.getKey());
for(var e:part.getValue().entrySet()){
var actual=chunk.getBlockState(e.getKey());require(actual==e.getValue(),"Monument mismatch "+e.getKey()+": "+actual+" != "+e.getValue());blocks++;
}
}
for(var a:p.approach()){
require(level.getBlockState(a).is(Blocks.TUFF_BRICK_STAIRS)&&level.getBlockState(a.above()).isAir()&&level.getBlockState(a.above(2)).isAir(),"Blocked stairs");
for(int y=a.getY()-1;y>=RegionalRelay215.ground(i,a.getX(),a.getZ());y--)require(level.getBlockState(new BlockPos(a.getX(),y,a.getZ())).isSolidRender(),"Floating approach");
}
var failure=RelayOffers199.offer(level,p.anchor(),new ItemStack(Items.DIRT),()->true);
try{failure.join();throw new AssertionError("Wrong payment accepted");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Raised relay is not recognized: "+expected);}
require(!SouthVillage214.active(i)&&SouthVillage214.cached(i)==null,"Village generated in 215");
RegionalRelay215.release(i);var rebuilt=RegionalRelay215.plan(i);require(p.chunks().equals(rebuilt.chunks()),"Rebuilt plan changed");
r.result().put("monument",Map.of("anchor",p.anchor().toShortString(),"base",p.origin().toShortString(),"chunks",touched.size(),"checkedBlocks",blocks,"foundation",p.maxFoundationDepth(),"approachSteps",p.approach().size(),"preparedChunks",adapter.preparationChunks(i).size()));
BlockPos camera=p.anchor().offset(22,11,r.south()?-24:24);
var visit=new LinkedHashMap<String,Object>();visit.put("relay",p.anchor().toShortString());visit.put("entrance",p.anchor().toShortString());visit.put("camera",camera.toShortString());
if(r.south()){
var t=SouthTemple213.cached(i);var f=SouthTerrain213.field(i);
// Seed 0's derived South has no sufficiently buried site in the retained 215 recipe.
// Keep the known seed-42 pyramid mandatory while exercising vegetation on that absence case.
require(t!=null||i.generation()==216&&level.getSeed()==0,"Missing widened pyramid");
if(t==null)r.result().put("temple",Map.of("present",false,"reason","no_buried_site"));
else {
int chests=0,tnt=0,enchanted=0,plates=0,torches=0;
for(var e:SouthTemple213.blueprint(i).entrySet()){
var at=t.at(e.getKey());var expected=e.getValue().is(Blocks.TERRACOTTA)?f.colour(at.getX(),at.getY(),at.getZ()):e.getValue().rotate(t.rotation());
require(level.getBlockState(at).getBlock()==expected.getBlock(),"Pyramid mismatch at "+at);
if(expected.is(Blocks.TNT))tnt++;
if(expected.is(Blocks.STONE_PRESSURE_PLATE))plates++;if(expected.is(Blocks.TORCH))torches++;
if(expected.is(Blocks.STONE_PRESSURE_PLATE))require(level.getBlockState(at.below(2)).is(Blocks.TNT)&&level.getBlockState(at.below()).isSolidRender(),"Disconnected trap");
}
for(var at:t.chests()){
require(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.ChestBlockEntity,"Missing chest");
var chest=(net.minecraft.world.level.block.entity.ChestBlockEntity)level.getBlockEntity(at);chest.unpackLootTable(null);chests++;
for(int slot=0;slot<chest.getContainerSize();slot++)if(chest.getItem(slot).isEnchanted())enchanted++;
}
require(chests==10&&tnt==SouthTemple213.blueprint(i).values().stream().filter(b->b.is(Blocks.TNT)).count()&&enchanted>=10,"Labyrinth loot/traps changed");
r.result().put("temple",Map.of("origin",t.origin().toShortString(),"rotation",t.rotation().name(),"width",31,"chests",chests,"tnt",tnt,"enchantedStacks",enchanted,"pressurePlates",plates,"torches",torches));
if(i.generation()==216)require(torches==0&&plates>=30,"Missing dark traps");
visit.put("temple",t.origin().toShortString());
}
if(i.generation()==216)South216Checks.verify(level,i,r.result(),visit);
visit.put("arrival",f.arrival().toShortString());
}else visit.put("arrival",adapter.village(i).arrival().toShortString());
r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("RELAYS215_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,93 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South209Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South209Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==209,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south209");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain209.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain209.field(island);
require(adapter.preparationChunks(island).size()<=64,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
int valleys=0,mesas=0;double low=999,high=0;BlockPos canyon=null;double best=0;
for(int x=-320;x<=320;x+=16)for(int z=-320;z<=320;z+=16){
int wx=island.centerX()+x,wz=island.centerZ()+z;var c=f.shape.column(wx,wz);if(c.edge()<60)continue;
low=Math.min(low,c.top());high=Math.max(high,c.top());if(c.valley()>.9)valleys++;if(c.valley()<.1)mesas++;
double slope=Math.abs(c.top()-f.shape.column(wx+16,wz).top());
if(slope>best){best=slope;canyon=new BlockPos(wx,(int)c.top()+24,wz);}
}
require(valleys>80&&mesas>80&&high-low>95,"Missing mesas / valleys");markers.add(canyon);
for(int dx:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,210,island.centerZ()));
var colours=new HashSet<String>();int sand=0,blocks=0,unsupported=0,biomes=0;
for(var marker:markers){
for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.shape.column(x,z);
int top=chunk.getHeight(Heightmap.Types.OCEAN_FLOOR,x&15,z&15);
require(Math.abs(top-f.top(x,z))<12,"Native surface diverges from density at "+x+","+z+": "+top+"/"+f.top(x,z));
require(level.getBiome(new BlockPos(x,top,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands209"),"Wrong chunk biome");biomes++;
for(int y=(int)c.bottom()-6;y<=top;y++){
var p=new BlockPos(x,y,z);var state=chunk.getBlockState(p);if(state.isAir())continue;blocks++;
var id=net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if(id.endsWith("_terracotta"))colours.add(id);
if(state.getBlock() instanceof ConcretePowderBlock||state.is(Blocks.RED_SAND)){
sand++;if(chunk.getBlockState(p.below()).isAir())unsupported++;
}
}
}
}
require(colours.size()==16&&sand>10&&unsupported==0,"Palette/support: "+colours.size()+" colours, "+sand+" sand, "+unsupported+" unsupported");
r.result().put("terrain",Map.of("low",low,"high",high,"valleys",valleys,"mesas",mesas,"sampledBlocks",blocks,"colours",colours.size(),"sand",sand,"unsupportedPowder",unsupported,"biomeChecks",biomes));
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH209_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,128 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South210Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South210Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==210,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south210");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain210.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain210.field(island);
require(adapter.preparationChunks(island).size()<=64,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
int valleys=0,mesas=0;double low=999,high=0;BlockPos canyon=null;double best=0;
var familySites=new EnumMap<SouthPalette210.Family,BlockPos>(SouthPalette210.Family.class);
for(int x=-320;x<=320;x+=16)for(int z=-320;z<=320;z+=16){
int wx=island.centerX()+x,wz=island.centerZ()+z;var c=f.shape.column(wx,wz);if(c.edge()<60)continue;
low=Math.min(low,c.top());high=Math.max(high,c.top());if(c.valley()>.9)valleys++;if(c.valley()<.1)mesas++;
if(c.valley()<.1)familySites.putIfAbsent(f.palette.family(c.massif()),new BlockPos(wx,(int)c.top(),wz));
double slope=Math.abs(c.top()-f.shape.column(wx+16,wz).top());
if(slope>best){best=slope;canyon=new BlockPos(wx,(int)c.top()+24,wz);}
}
require(valleys>80&&mesas>80&&high-low>95,"Missing mesas / valleys");markers.add(canyon);
require(familySites.size()==4,"Missing family sampling sites");markers.addAll(familySites.values());
for(int dx:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,210,island.centerZ()));
var colours=new HashSet<String>();int sand=0,blocks=0,unsupported=0,biomes=0;
for(var marker:markers){
for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.shape.column(x,z);
int top=chunk.getHeight(Heightmap.Types.OCEAN_FLOOR,x&15,z&15);
require(Math.abs(top-f.top(x,z))<12,"Native surface diverges from density at "+x+","+z+": "+top+"/"+f.top(x,z));
require(level.getBiome(new BlockPos(x,top,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands210"),"Wrong chunk biome");biomes++;
for(int y=(int)c.bottom()-6;y<=top;y++){
var p=new BlockPos(x,y,z);var state=chunk.getBlockState(p);if(state.isAir())continue;blocks++;
var id=net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
if(id.endsWith("terracotta"))colours.add(id);
require(!(state.getBlock() instanceof ConcretePowderBlock),"Historical coloured powder leaked into South");
if(state.is(Blocks.RED_SAND)){
sand++;if(chunk.getBlockState(p.below()).isAir())unsupported++;
}
}
}
}
require(colours.size()>=4&&sand>0&&unsupported==0,"Palette/support: "+colours.size()+" colours, "+sand+" sand, "+unsupported+" unsupported");
var ecology=new LinkedHashMap<String,Object>();
for(boolean valley:new boolean[]{true,false}){
BlockPos spot=null;double bestSpot=-999;
for(int dx=-288;dx<=288;dx+=16)for(int dz=-288;dz<=288;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;var c=f.shape.column(x,z);
if(c.edge()<100||f.slope(x,z)>1.5||(valley?c.valley()<.999:c.valley()>.001))continue;
var cover=f.palette.cover(c,dx,dz);if(f.palette.ground(cover,c,(int)c.top(),0)==null)continue;
double score=c.edge()-Math.abs(dx)*.2;if(score>bestSpot){bestSpot=score;spot=new BlockPos(x,(int)c.top(),z);}
}
require(spot!=null,"No ecology sampling site");int plants=0,soils=0,dryGrass=0,bush=0,cacti=0,flatValley=0,soilValley=0;
for(int dx=-24;dx<24;dx++)for(int dz=-24;dz<24;dz++){
int x=spot.getX()+dx,z=spot.getZ()+dz;level.getChunk(x>>4,z>>4);
int y=level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,x,z);var pos=new BlockPos(x,y,z);
// Plants may be ignored by this heightmap: inspect both just above and below it.
for(int py=y+2;py>=y-3;py--){var p=new BlockPos(x,py,z);var state=level.getBlockState(p);
if(state.is(Blocks.DEAD_BUSH)||state.is(Blocks.SHORT_DRY_GRASS)||state.is(Blocks.TALL_DRY_GRASS)||state.is(Blocks.CACTUS)){
require(state.canSurvive(level,p),"Unsupported dry plant");plants++;
if(state.is(Blocks.DEAD_BUSH))bush++;else if(state.is(Blocks.CACTUS))cacti++;else dryGrass++;
}
}
while(y>1&&(level.getBlockState(pos).isAir()||isPlant(level.getBlockState(pos).getBlock()))){pos=pos.below();y--;}
var state=level.getBlockState(pos);boolean soil=state.is(Blocks.COARSE_DIRT)||state.is(Blocks.DIRT)||state.is(Blocks.RED_SAND);
if(soil)soils++;var c=f.shape.column(x,z);
if(c.edge()>24&&c.valley()>.999&&f.slope(x,z)<=2.8){flatValley++;if(soil)soilValley++;}
}
require(plants>3&&dryGrass>0&&bush>0&&soils>30,"No dry ecology in "+(valley?"valley":"mesa")+": "+plants+" plants, "+soils+" soils");
require(!valley||flatValley>0&&soilValley>=flatValley*.95,"Discontinuous soil in the flat valley");
ecology.put(valley?"valley":"mesa",Map.of("site",spot.toShortString(),"plants",plants,"dryGrass",dryGrass,"bushes",bush,"cacti",cacti,"soilColumns",soils,"flatValley",flatValley,"soilValley",soilValley));
}
r.result().put("ecology",ecology);
r.result().put("terrain",Map.of("low",low,"high",high,"valleys",valleys,"mesas",mesas,"sampledBlocks",blocks,"colours",colours.size(),"sand",sand,"unsupportedPowder",unsupported,"biomeChecks",biomes));
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH210_CHECK_FAILED",e);server.halt(false);}
}
private static boolean isPlant(Block b){return b==Blocks.DEAD_BUSH||b==Blocks.SHORT_DRY_GRASS||b==Blocks.TALL_DRY_GRASS||b==Blocks.CACTUS;}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,103 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South211Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South211Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==211,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south211");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain211.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain211.field(island);
require(adapter.preparationChunks(island).size()<=64,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
double low=999,high=-1;int rockColumns=0,openings=0;BlockPos canyon=null;double best=-999;
var quadrants=new HashSet<Integer>();
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;int top=f.top(x,z);var c=f.column(x,z);
if(top>0){rockColumns++;low=Math.min(low,top);high=Math.max(high,top);}
if(c.roof()>120&&f.density(x,c.roof()-25,z)<=0){openings++;quadrants.add((dx<0?0:1)+(dz<0?0:2));}
// View a deep exposed cut with some warm rock still on its lip.
if(top>140&&c.edge()>80){
double score=Math.abs(top-f.top(x+16,z))+Math.abs(top-f.top(x,z+16));
if(score>best){best=score;canyon=new BlockPos(x,top+16,z);}
}
}
require(rockColumns>300&&openings>30&&quadrants.size()==4&&high<320&&high>180,"Missing bounded shattered relief: "+rockColumns+"/"+openings+"/"+high);
markers.add(canyon);for(int dx:new int[]{-192,0,192})for(int dz:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,220,island.centerZ()+dz));
int blocks=0,biomes=0,sand=0,unsupported=0,exposedCore=0,core=0,skin=0,plants=0;
var colours=new HashSet<String>();BlockPos colourSite=null;
for(var marker:markers)for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
require(level.getBiome(new BlockPos(x,210,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands211"),"Wrong region biome");biomes++;
for(int y=Math.max(1,(int)c.bottom()-10);y<=c.roof()+4;y++){
var pos=new BlockPos(x,y,z);var state=chunk.getBlockState(pos);if(state.isAir())continue;blocks++;
if(state.is(Blocks.RED_SAND)){sand++;if(chunk.getBlockState(pos.below()).isAir())unsupported++;}
if(isPlant(state.getBlock())){require(state.canSurvive(level,pos),"Unsupported dry plant");plants++;continue;}
if(y>=c.roof()-10||y<=c.bottom()+10){
require(!isCool(state.getBlock()),"Cool core colour exposed on external skin");skin++;
}
if(isCool(state.getBlock())){
core++;colours.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
boolean exposed=false;for(var dir:net.minecraft.core.Direction.values())if(level.getBlockState(pos.relative(dir)).isAir()){exposed=true;break;}
if(exposed){exposedCore++;if(colourSite==null)colourSite=pos;}
}
}
}
require(sand>10&&unsupported==0&&core>30&&exposedCore>0&&skin>100&&plants>0,"Materials: sand="+sand+", unsupported="+unsupported+", core="+core+", exposed="+exposedCore+", plants="+plants);
r.result().put("terrain",Map.of("low",low,"high",high,"rockColumns",rockColumns,"openings",openings,"sampledBlocks",blocks,"biomeChecks",biomes));
r.result().put("materials",Map.of("sand",sand,"unsupported",unsupported,"coolCore",core,"exposedCore",exposedCore,"skin",skin,"plants",plants,"coolColours",colours));
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString(),"colour",colourSite.toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH211_CHECK_FAILED",e);server.halt(false);}
}
private static boolean isCool(Block b){for(var c:List.of(DyeColor.LIME,DyeColor.CYAN,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA))if(b==Blocks.DYED_TERRACOTTA.pick(c))return true;return false;}
private static boolean isPlant(Block b){return b==Blocks.DEAD_BUSH||b==Blocks.SHORT_DRY_GRASS||b==Blocks.TALL_DRY_GRASS||b==Blocks.CACTUS;}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,117 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South212Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South212Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==212,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south212");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain212.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain212.field(island);
require(adapter.preparationChunks(island).size()<=90,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
double low=999,high=-1;int rockColumns=0,openings=0,raised=0;double maxUplift=0;BlockPos canyon=null;double best=-999;
var quadrants=new HashSet<Integer>();
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;int top=f.top(x,z);var c=f.column(x,z);
if(c.uplift()>8)raised++;maxUplift=Math.max(maxUplift,c.uplift());
if(top>0){rockColumns++;low=Math.min(low,top);high=Math.max(high,top);}
if(c.roof()>120&&f.density(x,c.roof()-25,z)<=0){openings++;quadrants.add((dx<0?0:1)+(dz<0?0:2));}
// View a deep exposed cut with some warm rock still on its lip.
if(top>140&&c.edge()>80){
double score=Math.abs(top-f.top(x+16,z))+Math.abs(top-f.top(x,z+16));
if(score>best){best=score;canyon=new BlockPos(x,top+16,z);}
}
}
require(rockColumns>300&&openings>30&&quadrants.size()==4&&high<320&&high>180,"Missing bounded shattered relief: "+rockColumns+"/"+openings+"/"+high);
require(raised>15&&maxUplift>20&&high>270,"Weak native crowns: "+raised+"/"+maxUplift+"/"+high);
markers.add(canyon);for(int dx:new int[]{-192,0,192})for(int dz:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,220,island.centerZ()+dz));
for(int radius:new int[]{180,240,300,360,420})markers.add(new BlockPos(island.centerX()+radius,240,island.centerZ()));
int blocks=0,biomes=0,sand=0,unsupported=0,exposed=0,plants=0,logs=0,leaves=0;
var colours=new HashSet<String>();BlockPos colourSite=null;
for(var marker:markers)for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
require(level.getBiome(new BlockPos(x,210,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands212"),"Wrong biome");biomes++;
for(int y=Math.max(1,(int)c.bottom()-10);y<=c.roof()+14;y++){
var pos=new BlockPos(x,y,z);var state=chunk.getBlockState(pos);if(state.isAir())continue;blocks++;
if(state.is(Blocks.SAND)){sand++;if(chunk.getBlockState(pos.below()).isAir())unsupported++;}
if(isPlant(state.getBlock())){require(state.canSurvive(level,pos),"Unsupported dry plant");plants++;}
if(state.is(Blocks.ACACIA_LOG)||state.is(Blocks.STRIPPED_ACACIA_LOG))logs++;
if(state.is(Blocks.ACACIA_LEAVES))leaves++;
if(isCool(state.getBlock())){
boolean visible=false;for(var dir:net.minecraft.core.Direction.values())if(level.getBlockState(pos.relative(dir)).isAir()){visible=true;break;}
if(visible){exposed++;colours.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());if(colourSite==null)colourSite=pos;}
}
}
}
require(sand>10&&unsupported==0&&exposed>20&&colours.size()>=3&&plants>0&&logs>0&&leaves>0,"Materials: sand="+sand+", unsupported="+unsupported+", exposed="+exposed+", colours="+colours+", trees="+logs+"/"+leaves);
var temple=SouthTemple212.cached(island);require(temple!=null,"Missing long complex");int tnt=0,chests=0,enchantments=0,lootStacks=0;
for(var e:SouthTemple212.blueprint().entrySet()){
var local=e.getKey();var at=temple.at(local.getX(),local.getY(),local.getZ());level.getChunkAt(at);
if(e.getValue().is(Blocks.TNT)){require(level.getBlockState(at).is(Blocks.TNT),"TNT missing at "+at);tnt++;}
if(e.getValue().is(Blocks.STONE_PRESSURE_PLATE)){require(level.getBlockState(at).is(Blocks.STONE_PRESSURE_PLATE)&&level.getBlockState(at.below()).isSolidRender()&&level.getBlockState(at.below(2)).is(Blocks.TNT),"Broken trap circuit");}
}
for(var at:temple.chests()){
require(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.ChestBlockEntity,"Missing cache "+at);
var chest=(net.minecraft.world.level.block.entity.ChestBlockEntity)level.getBlockEntity(at);chest.unpackLootTable(null);chests++;
for(int slot=0;slot<chest.getContainerSize();slot++){var stack=chest.getItem(slot);if(!stack.isEmpty()){lootStacks++;if(stack.isEnchanted())enchantments++;}}
}
require(tnt==108&&chests==8&&enchantments>=8&&lootStacks>=24,"Incomplete temple loot / traps");
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"eastWest",temple.eastWest(),"naturalGroundFraction",temple.groundCoverage(),"tnt",tnt,"chests",chests,"enchantedStacks",enchantments,"lootStacks",lootStacks));
r.result().put("terrain",Map.of("low",low,"high",high,"rockColumns",rockColumns,"openings",openings,"sampledBlocks",blocks,"biomeChecks",biomes,"raisedColumns",raised,"maxUplift",maxUplift));
r.result().put("materials",Map.of("sand",sand,"unsupported",unsupported,"exposedColour",exposed,"colours",colours,"plants",plants,"logs",logs,"leaves",leaves));
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString(),"colour",colourSite.toShortString(),"temple",temple.origin().toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH212_CHECK_FAILED",e);server.halt(false);}
}
private static boolean isCool(Block b){for(var c:List.of(DyeColor.LIME,DyeColor.CYAN,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA))if(b==Blocks.DYED_TERRACOTTA.pick(c))return true;return false;}
private static boolean isPlant(Block b){return b==Blocks.DEAD_BUSH||b==Blocks.SHORT_DRY_GRASS||b==Blocks.TALL_DRY_GRASS||b==Blocks.CACTUS;}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,125 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South213Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South213Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==213,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south213");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain213.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain213.field(island);
require(adapter.preparationChunks(island).size()<=90,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
double low=999,high=-1;int rockColumns=0,openings=0,raised=0;double maxUplift=0;BlockPos canyon=null;double best=-999;
var quadrants=new HashSet<Integer>();
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;int top=f.top(x,z);var c=f.column(x,z);
if(c.uplift()>8)raised++;maxUplift=Math.max(maxUplift,c.uplift());
if(top>0){rockColumns++;low=Math.min(low,top);high=Math.max(high,top);}
if(c.roof()>120&&f.density(x,c.roof()-25,z)<=0){openings++;quadrants.add((dx<0?0:1)+(dz<0?0:2));}
// View a deep exposed cut with some warm rock still on its lip.
if(top>140&&c.edge()>80){
double score=Math.abs(top-f.top(x+16,z))+Math.abs(top-f.top(x,z+16));
if(score>best){best=score;canyon=new BlockPos(x,top+16,z);}
}
}
require(rockColumns>300&&openings>30&&quadrants.size()==4&&high<320&&high>180,"Missing bounded shattered relief: "+rockColumns+"/"+openings+"/"+high);
require(raised>15&&maxUplift>20&&high>270,"Weak native crowns: "+raised+"/"+maxUplift+"/"+high);
markers.add(canyon);for(int dx:new int[]{-192,0,192})for(int dz:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,220,island.centerZ()+dz));
for(int radius:new int[]{180,240,300,360,420})markers.add(new BlockPos(island.centerX()+radius,240,island.centerZ()));
int blocks=0,biomes=0,sand=0,unsupported=0,exposed=0,plants=0,logs=0,leaves=0;
var colours=new HashSet<String>();BlockPos colourSite=null;
for(var marker:markers)for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
require(level.getBiome(new BlockPos(x,210,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands213"),"Wrong biome");biomes++;
for(int y=Math.max(1,(int)c.bottom()-10);y<=c.roof()+14;y++){
var pos=new BlockPos(x,y,z);var state=chunk.getBlockState(pos);if(state.isAir())continue;blocks++;
if(state.is(Blocks.SAND)||state.is(Blocks.RED_SAND)){sand++;if(chunk.getBlockState(pos.below()).isAir())unsupported++;}
if(isPlant(state.getBlock())){require(state.canSurvive(level,pos),"Unsupported dry plant");plants++;}
if(state.is(Blocks.ACACIA_LOG)||state.is(Blocks.STRIPPED_ACACIA_LOG))logs++;
if(state.is(Blocks.ACACIA_LEAVES))leaves++;
if(isCool(state.getBlock())){
boolean visible=false;for(var dir:net.minecraft.core.Direction.values())if(level.getBlockState(pos.relative(dir)).isAir()){visible=true;break;}
if(visible){exposed++;colours.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());if(colourSite==null)colourSite=pos;}
}
}
}
require(sand==0&&exposed>20&&colours.size()>=3&&logs==0&&leaves==0,"Materials: sand="+sand+", unsupported="+unsupported+", exposed="+exposed+", colours="+colours+", trees="+logs+"/"+leaves);
var temple=SouthTemple213.cached(island);require(temple!=null,"Missing buried complex");int tnt=0,chests=0,enchantments=0,lootStacks=0;
for(var e:SouthTemple213.blueprint().entrySet()){
var local=e.getKey();var at=temple.at(local.getX(),local.getY(),local.getZ());level.getChunkAt(at);
if(e.getValue().is(Blocks.TNT)){require(level.getBlockState(at).is(Blocks.TNT),"TNT missing at "+at);tnt++;}
if(e.getValue().is(Blocks.STONE_PRESSURE_PLATE)){require(level.getBlockState(at).is(Blocks.STONE_PRESSURE_PLATE)&&level.getBlockState(at.below()).isSolidRender()&&level.getBlockState(at.below(2)).is(Blocks.TNT),"Broken trap circuit");}
}
for(var room:SouthTemple213.ROOMS){
var at=temple.at(room.centre());require(at.getY()+6<=f.top(at.getX(),at.getZ()),"Exposed underground room");
require(level.getBlockState(at.above(2)).isAir(),"Room headroom filled");
}
for(var e:SouthTemple213.blueprint().entrySet())if(e.getKey().getY()>=0&&e.getValue().is(Blocks.TERRACOTTA)){
var at=temple.at(e.getKey());require(level.getBlockState(at)==f.colour(at.getX(),at.getY(),at.getZ()),"Pyramid colour doesn't match local rock");
}
for(var at:temple.chests()){
require(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.ChestBlockEntity,"Missing cache "+at);
var chest=(net.minecraft.world.level.block.entity.ChestBlockEntity)level.getBlockEntity(at);chest.unpackLootTable(null);chests++;
for(int slot=0;slot<chest.getContainerSize();slot++){var stack=chest.getItem(slot);if(!stack.isEmpty()){lootStacks++;if(stack.isEnchanted())enchantments++;}}
}
require(tnt==24&&chests==10&&enchantments>=10&&lootStacks>=30,"Incomplete temple loot / traps");
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"rotation",temple.rotation().name(),"buriedRockFraction",temple.rockFraction(),"tnt",tnt,"chests",chests,"enchantedStacks",enchantments,"lootStacks",lootStacks));
r.result().put("terrain",Map.of("low",low,"high",high,"rockColumns",rockColumns,"openings",openings,"sampledBlocks",blocks,"biomeChecks",biomes,"raisedColumns",raised,"maxUplift",maxUplift));
r.result().put("materials",Map.of("sand",sand,"unsupported",unsupported,"exposedColour",exposed,"colours",colours,"plants",plants,"logs",logs,"leaves",leaves));
var outside=temple.at(-1,0,-38);var camera=new BlockPos(outside.getX(),Math.max(temple.origin().getY()+14,f.top(outside.getX(),outside.getZ())+6),outside.getZ());
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString(),"colour",colourSite.toShortString(),"temple",temple.origin().toShortString(),"entrance",temple.entrance().toShortString(),"camera",camera.toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH213_CHECK_FAILED",e);server.halt(false);}
}
private static boolean isCool(Block b){for(var c:List.of(DyeColor.LIME,DyeColor.CYAN,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA))if(b==Blocks.DYED_TERRACOTTA.pick(c))return true;return false;}
private static boolean isPlant(Block b){return b==Blocks.DEAD_BUSH||b==Blocks.SHORT_DRY_GRASS||b==Blocks.TALL_DRY_GRASS||b==Blocks.CACTUS;}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,159 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** Actual South offering and restart on disposable worlds only, then bounded native terrain samples. */
final class South214Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(South214Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==214,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","south214");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==4).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing South anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(4);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate search accepted");
result.put("anchor",pos.toShortString());run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(SouthTerrain213.active(admitted)&&admitted.diameter()==1024&&admitted.climate().equals("arid"),"Wrong region identity");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter()));
if(r.fresh()){
require(r.payment().getCount()==1,"Wrong charge");require(session.island(admitted.id()).status().equals("reserved"),"Expected interrupted preparation");
r.result().put("interruptedProgress",session.progress());finish(r);return;
}
var island=session.island(admitted.id());
if(!island.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var f=SouthTerrain213.field(island);
require(adapter.preparationChunks(island).size()<=125,"Unbounded preparation");
var relay=f.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK)&&level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==4,"Missing South continuation");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int depth=1;depth<=7;depth++)
require(!level.getBlockState(relay.offset(x,-depth,z)).isAir(),"Relay footing hangs over air");
var markers=new ArrayList<BlockPos>();markers.add(f.arrival());markers.add(relay);
double low=999,high=-1;int rockColumns=0,openings=0,raised=0;double maxUplift=0;BlockPos canyon=null;double best=-999;
var quadrants=new HashSet<Integer>();
for(int dx=-320;dx<=320;dx+=16)for(int dz=-320;dz<=320;dz+=16){
int x=island.centerX()+dx,z=island.centerZ()+dz;int top=f.top(x,z);var c=f.column(x,z);
if(c.uplift()>8)raised++;maxUplift=Math.max(maxUplift,c.uplift());
if(top>0){rockColumns++;low=Math.min(low,top);high=Math.max(high,top);}
if(c.roof()>120&&f.density(x,c.roof()-25,z)<=0){openings++;quadrants.add((dx<0?0:1)+(dz<0?0:2));}
// View a deep exposed cut with some warm rock still on its lip.
if(top>140&&c.edge()>80){
double score=Math.abs(top-f.top(x+16,z))+Math.abs(top-f.top(x,z+16));
if(score>best){best=score;canyon=new BlockPos(x,top+16,z);}
}
}
require(rockColumns>300&&openings>30&&quadrants.size()==4&&high<320&&high>180,"Missing bounded shattered relief: "+rockColumns+"/"+openings+"/"+high);
require(raised>15&&maxUplift>20&&high>270,"Weak native crowns: "+raised+"/"+maxUplift+"/"+high);
markers.add(canyon);for(int dx:new int[]{-192,0,192})for(int dz:new int[]{-192,0,192})markers.add(new BlockPos(island.centerX()+dx,220,island.centerZ()+dz));
for(int radius:new int[]{180,240,300,360,420})markers.add(new BlockPos(island.centerX()+radius,240,island.centerZ()));
int blocks=0,biomes=0,sand=0,unsupported=0,exposed=0,plants=0,logs=0,leaves=0;
var colours=new HashSet<String>();BlockPos colourSite=null;
for(var marker:markers)for(int a=-8;a<=8;a+=4)for(int b=-8;b<=8;b+=4){
int x=marker.getX()+a,z=marker.getZ()+b;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
require(level.getBiome(new BlockPos(x,210,z)).unwrapKey().orElseThrow().identifier().toString().equals("sanctuary:south_badlands213"),"Wrong biome");biomes++;
for(int y=Math.max(1,(int)c.bottom()-10);y<=c.roof()+14;y++){
var pos=new BlockPos(x,y,z);var state=chunk.getBlockState(pos);if(state.isAir())continue;blocks++;
if(state.is(Blocks.SAND)||state.is(Blocks.RED_SAND)){sand++;if(chunk.getBlockState(pos.below()).isAir())unsupported++;}
if(isPlant(state.getBlock())){require(state.canSurvive(level,pos),"Unsupported dry plant");plants++;}
if(state.is(Blocks.ACACIA_LOG)||state.is(Blocks.STRIPPED_ACACIA_LOG))logs++;
if(state.is(Blocks.ACACIA_LEAVES))leaves++;
if(isCool(state.getBlock())){
boolean visible=false;for(var dir:net.minecraft.core.Direction.values())if(level.getBlockState(pos.relative(dir)).isAir()){visible=true;break;}
if(visible){exposed++;colours.add(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());if(colourSite==null)colourSite=pos;}
}
}
}
require(sand==0&&exposed>20&&colours.size()>=3&&logs==0&&leaves==0,"Materials: sand="+sand+", unsupported="+unsupported+", exposed="+exposed+", colours="+colours+", trees="+logs+"/"+leaves);
var temple=SouthTemple213.cached(island);require(temple!=null,"Missing buried complex");int tnt=0,chests=0,enchantments=0,lootStacks=0;
for(var e:SouthTemple213.blueprint().entrySet()){
var local=e.getKey();var at=temple.at(local.getX(),local.getY(),local.getZ());level.getChunkAt(at);
if(e.getValue().is(Blocks.TNT)){require(level.getBlockState(at).is(Blocks.TNT),"TNT missing at "+at);tnt++;}
if(e.getValue().is(Blocks.STONE_PRESSURE_PLATE)){require(level.getBlockState(at).is(Blocks.STONE_PRESSURE_PLATE)&&level.getBlockState(at.below()).isSolidRender()&&level.getBlockState(at.below(2)).is(Blocks.TNT),"Broken trap circuit");}
}
for(var room:SouthTemple213.ROOMS){
var at=temple.at(room.centre());require(at.getY()+6<=f.top(at.getX(),at.getZ()),"Exposed underground room");
require(level.getBlockState(at.above(2)).isAir(),"Room headroom filled");
}
for(var e:SouthTemple213.blueprint().entrySet())if(e.getKey().getY()>=0&&e.getValue().is(Blocks.TERRACOTTA)){
var at=temple.at(e.getKey());require(level.getBlockState(at)==f.colour(at.getX(),at.getY(),at.getZ()),"Pyramid colour doesn't match local rock");
}
for(var at:temple.chests()){
require(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.ChestBlockEntity,"Missing cache "+at);
var chest=(net.minecraft.world.level.block.entity.ChestBlockEntity)level.getBlockEntity(at);chest.unpackLootTable(null);chests++;
for(int slot=0;slot<chest.getContainerSize();slot++){var stack=chest.getItem(slot);if(!stack.isEmpty()){lootStacks++;if(stack.isEnchanted())enchantments++;}}
}
require(tnt==24&&chests==10&&enchantments>=10&&lootStacks>=30,"Incomplete temple loot / traps");
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"rotation",temple.rotation().name(),"buriedRockFraction",temple.rockFraction(),"tnt",tnt,"chests",chests,"enchantedStacks",enchantments,"lootStacks",lootStacks));
r.result().put("terrain",Map.of("low",low,"high",high,"rockColumns",rockColumns,"openings",openings,"sampledBlocks",blocks,"biomeChecks",biomes,"raisedColumns",raised,"maxUplift",maxUplift));
r.result().put("materials",Map.of("sand",sand,"unsupported",unsupported,"exposedColour",exposed,"colours",colours,"plants",plants,"logs",logs,"leaves",leaves));
var town=SouthVillage214.cached(island);require(town!=null,"Missing surface village");
int wooden=0,lamps=0,webs=0;var touched=new HashSet<net.minecraft.world.level.ChunkPos>();
for(var part:town.chunks().entrySet()){
var chunk=level.getChunk(net.minecraft.world.level.ChunkPos.unpack(part.getKey()).x(),net.minecraft.world.level.ChunkPos.unpack(part.getKey()).z());touched.add(chunk.getPos());
for(var e:part.getValue().entrySet()){
var actual=chunk.getBlockState(e.getKey());var expected=e.getValue();
require(actual.getBlock()==expected.getBlock(),"Village mismatch "+e.getKey()+": "+actual+" != "+expected);
if(actual.is(Blocks.DARK_OAK_PLANKS))wooden++;
if(actual.is(Blocks.LANTERN)){require(actual.canSurvive(level,e.getKey()),"Unsupported lantern");lamps++;}
if(actual.is(Blocks.COBWEB))webs++;
}
}
require(town.houses().size()>=7&&town.houses().size()<=8&&lamps>=4&&webs>=7,"Incomplete village");
var connected=villagePaths(town);
for(var house:town.houses()){
require(connected.contains(house.entrance())&&connected.contains(house.at(0,1,3)),"Village street fails to reach an interior");
require(level.getBlockState(house.entrance()).isAir()&&level.getBlockState(house.entrance().above()).isAir(),"Blocked village doorway");
for(int x=-house.halfWidth();x<=house.halfWidth();x++)for(int z=0;z<=house.depth();z++){
var at=house.at(x,0,z);for(int depth=1;depth<=Math.max(1,at.getY()-f.top(at.getX(),at.getZ()));depth++)require(!level.getBlockState(at.below(depth)).isAir(),"Hanging foundation "+at);
}
}
r.result().put("village",Map.of("centre",town.centre().toShortString(),"rotation",town.rotation().name(),"houses",town.houses().size(),"chunks",touched.size(),"planks",wooden,"lanterns",lamps,"cobwebs",webs));
var outside=temple.at(-1,0,-38);var camera=new BlockPos(outside.getX(),Math.max(temple.origin().getY()+14,f.top(outside.getX(),outside.getZ())+6),outside.getZ());
r.result().put("visit",Map.of("arrival",f.arrival().toShortString(),"relay",relay.toShortString(),"canyon",canyon.toShortString(),"colour",colourSite.toShortString(),"temple",temple.origin().toShortString(),"entrance",town.centre().above(4).toShortString(),"camera",new BlockPos(town.camera().getX(),Math.max(town.camera().getY(),f.top(town.camera().getX(),town.camera().getZ())+8),town.camera().getZ()).toShortString(),"village",town.centre().toShortString()));
finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("SOUTH214_CHECK_FAILED",e);server.halt(false);}
}
private static Set<BlockPos> villagePaths(SouthVillage214.Plan town){
var blocks=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();town.chunks().values().forEach(blocks::putAll);
var seen=new HashSet<BlockPos>();var todo=new ArrayDeque<BlockPos>();var start=town.street().getFirst().above();seen.add(start);todo.add(start);
while(!todo.isEmpty()){
var p=todo.removeFirst();for(var direction:List.of(net.minecraft.core.Direction.NORTH,net.minecraft.core.Direction.SOUTH,net.minecraft.core.Direction.EAST,net.minecraft.core.Direction.WEST))for(int dy=-1;dy<=1;dy++){
var next=p.relative(direction).offset(0,dy,0);var feet=blocks.get(next);var head=blocks.get(next.above());var floor=blocks.get(next.below());
if(feet==null||head==null||floor==null||!feet.isAir()||!head.isAir()||floor.isAir())continue;
if(seen.add(next))todo.add(next);
}
}
return seen;
}
private static boolean isCool(Block b){for(var c:List.of(DyeColor.LIME,DyeColor.CYAN,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA))if(b==Blocks.DYED_TERRACOTTA.pick(c))return true;return false;}
private static boolean isPlant(Block b){return b==Blocks.DEAD_BUSH||b==Blocks.SHORT_DRY_GRASS||b==Blocks.TALL_DRY_GRASS||b==Blocks.CACTUS;}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,56 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
/** Small native samples of a dense forest and walked surface routes, not a full-island pre-generation. */
final class South216Checks {
static void verify(ServerLevel level,ExpansionIsland i,Map<String,Object> result,Map<String,Object> visit){
var f=SouthTerrain213.field(i);var paths=SouthPaths216.plan(i);require(paths.routes().size()>=5,"Missing branches");
int checked=0;var chunks=new HashSet<Long>();
for(var route:paths.routes()){
var at=route.get(route.size()/2);var cp=new net.minecraft.world.level.ChunkPos(at.getX()>>4,at.getZ()>>4);if(!chunks.add(cp.pack()))continue;
var chunk=level.getChunk(cp.x(),cp.z());
for(var p:paths.chunks().get(cp.pack())){
require(chunk.getBlockState(p).is(Blocks.DIRT_PATH),"Missing native path "+p);
require(chunk.getBlockState(p.above()).isAir()&&chunk.getBlockState(p.above(2)).isAir(),"Blocked path "+p);
require(level.getBlockState(p.below()).isSolidRender(),"Unsupported path "+p);checked++;
}
}
require(checked>100,"Too few path samples");result.put("paths",Map.of("branches",paths.routes().size(),"plannedBlocks",paths.surface().size(),"terrainSamples",paths.samples(),"checkedBlocks",checked,"checkedChunks",chunks.size()));
BlockPos site=null;int best=-1;
for(int dx=-352;dx<=352;dx+=32)for(int dz=-352;dz<=352;dz+=32){
int x=i.centerX()+dx,z=i.centerZ()+dz;if(f.top(x,z)<150)continue;int score=0;
for(int a=-24;a<=24;a+=8)for(int b=-24;b<=24;b+=8){
int px=x+a,pz=z+b;
if(!f.desert(px,pz)&&f.slope(px,pz)<.8&&NorthTerrain200.noise(i.seed()+21213,(px-i.centerX())/39.0,(pz-i.centerZ())/39.0)>.35
&&!RegionalRelay215.reserved(i,px,pz)&&!SouthPaths216.near(i,px,pz,2))score++;
}
if(score>best){best=score;site=new BlockPos(x,f.top(x,z),z);}
}
require(site!=null&&best>=15,"No substantial soil patch");
int trunks=0,leaves=0,tall=0,roots=0,acacia=0;var canopy=new HashSet<Long>();
for(int x=site.getX()-24;x<=site.getX()+24;x++)for(int z=site.getZ()-24;z<=site.getZ()+24;z++){
var chunk=level.getChunk(x>>4,z>>4);int ground=f.top(x,z);if(ground<0)continue;
var base=new BlockPos(x,ground+1,z);var bottom=chunk.getBlockState(base.below());
// Horizontal limbs can touch a neighbouring ledge; they are not tree roots.
if(chunk.getBlockState(base).is(Blocks.OAK_LOG)&&chunk.getBlockState(base).getValue(net.minecraft.world.level.block.RotatedPillarBlock.AXIS)==net.minecraft.core.Direction.Axis.Y){
require(bottom.is(Blocks.DIRT)||bottom.is(Blocks.COARSE_DIRT)||bottom.is(Blocks.GRASS_BLOCK),"Oak not rooted in soil at "+base+": "+bottom);roots++;
int height=0;for(int y=0;y<28;y++)if(chunk.getBlockState(base.above(y)).is(Blocks.OAK_LOG))height=y+1;if(height>=8)tall++;
}
for(int y=ground+1;y<=ground+30;y++){
var b=chunk.getBlockState(new BlockPos(x,y,z));
if(b.is(Blocks.OAK_LOG))trunks++;
if(b.is(Blocks.OAK_LEAVES)){leaves++;canopy.add(net.minecraft.world.level.ChunkPos.pack(x,z));}
if(b.is(Blocks.ACACIA_LOG)||b.is(Blocks.ACACIA_LEAVES))acacia++;
}
}
require(roots>=12&&tall>=5&&leaves>800&&canopy.size()>600&&acacia==0,"Sparse forest: roots="+roots+" tall="+tall+" leaves="+leaves+" canopy="+canopy.size()+" acacia="+acacia);
result.put("forest",Map.of("site",site.toShortString(),"roots",roots,"tallOaks",tall,"logs",trunks,"leaves",leaves,"canopyColumns",canopy.size(),"soilScore",best,"acacia",acacia));
visit.put("forest",site.toShortString());visit.put("entrance",site.above(8).toShortString());visit.put("camera",site.offset(28,32,-30).toShortString());
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,79 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual anchor transaction and restart, followed by native monument and closed-water checks. */
final class West217Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(West217Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==217,"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","west217");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==6).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing West anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(6);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(WestOcean217.active(admitted)&&admitted.centerX()<0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong West placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=WestOcean217.field(i);var temple=WestTemple217.plan(i);
require(a.preparationChunks(i).size()<=110,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int gold=0,prismarine=0,lanterns=0,water=0;var b=temple.bounds();var pos=new BlockPos.MutableBlockPos();
for(int x=b.minX();x<=b.maxX();x++)for(int z=b.minZ();z<=b.maxZ();z++){
var c=f.column(x,z);require(level.getBlockState(pos.set(x,c.bottom(),z)).isSolidRender(),"Pierced foundation");
require(level.getBlockState(pos.set(x,WestOcean217.SEA,z)).is(Blocks.WATER),"Temple exposed");
for(int y=temple.origin().getY();y<=temple.origin().getY()+25;y++){
var s=level.getBlockState(pos.set(x,y,z));if(s.is(Blocks.GOLD_BLOCK))gold++;if(s.is(Blocks.PRISMARINE)||s.is(Blocks.PRISMARINE_BRICKS)||s.is(Blocks.DARK_PRISMARINE))prismarine++;if(s.is(Blocks.SEA_LANTERN))lanterns++;if(s.is(Blocks.WATER))water++;
}
}
require(gold==8&&prismarine>5000&&lanterns>10&&water>15000,"Native temple incomplete: gold="+gold+" prismarine="+prismarine+" lamps="+lanterns+" water="+water);
int edges=0,corals=0;var touched=new HashSet<Long>();
for(int n=0;n<8;n++){
double angle=n*Math.PI/4;BlockPos shore=null;
for(int rad=440;rad>=280;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.water()&&WestOcean217.SEA-c.floor()>=3){shore=new BlockPos(x,c.floor(),z);break;}}
require(shore!=null,"Missing coastal shallows");var cp=new net.minecraft.world.level.ChunkPos(shore.getX()>>4,shore.getZ()>>4);var chunk=level.getChunk(cp.x(),cp.z());touched.add(cp.pack());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(!c.water())continue;edges++;
require(chunk.getBlockState(pos.set(x,WestOcean217.SEA,z)).getFluidState().is(net.minecraft.tags.FluidTags.WATER),"Broken water table");
require(chunk.getBlockState(pos.set(x,c.floor()-3,z)).isSolidRender(),"Open seabed");
for(int y=c.floor()+1;y<WestOcean217.SEA;y++){var s=chunk.getBlockState(pos.set(x,y,z));if(s.is(net.minecraft.tags.BlockTags.CORALS))corals++;}
for(var dir:Direction.Plane.HORIZONTAL){int nx=x+dir.getStepX(),nz=z+dir.getStepZ();var other=f.column(nx,nz);if(!other.water())require(level.getBlockState(pos.set(nx,WestOcean217.SEA,nz)).isSolidRender(),"Open shore");}
}
}
require(edges>600&&corals>0,"Missing coast/warm coral: "+edges+" / "+corals);
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"goldBlocks",gold,"prismarine",prismarine,"lanterns",lanterns,"water",water));
r.result().put("ocean",Map.of("seaY",WestOcean217.SEA,"branches",f.branchCount(),"coastalColumns",edges,"coastalChunks",touched.size(),"coralBlocks",corals));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(0,36,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("WEST217_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual anchor transaction and restart, followed by native monument and closed-water checks. */
final class West218Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(West218Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==218,"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","west218");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==6).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing West anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(6);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(WestOcean218.active(admitted)&&admitted.centerX()<0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong West placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=WestOcean218.field(i);var temple=WestTemple217.plan(i);
require(a.preparationChunks(i).size()<=110,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int gold=0,prismarine=0,lanterns=0,water=0;var b=temple.bounds();var pos=new BlockPos.MutableBlockPos();
for(int x=b.minX();x<=b.maxX();x++)for(int z=b.minZ();z<=b.maxZ();z++){
var c=f.column(x,z);require(level.getBlockState(pos.set(x,c.bottom()+8,z)).isSolidRender(),"Pierced foundation");
require(level.getBlockState(pos.set(x,WestOcean218.SEA,z)).is(Blocks.WATER),"Temple exposed");
for(int y=temple.origin().getY();y<=temple.origin().getY()+25;y++){
var s=level.getBlockState(pos.set(x,y,z));if(s.is(Blocks.GOLD_BLOCK))gold++;if(s.is(Blocks.PRISMARINE)||s.is(Blocks.PRISMARINE_BRICKS)||s.is(Blocks.DARK_PRISMARINE))prismarine++;if(s.is(Blocks.SEA_LANTERN))lanterns++;if(s.is(Blocks.WATER))water++;
}
}
require(gold==8&&prismarine>5000&&lanterns>10&&water>15000,"Native temple incomplete: gold="+gold+" prismarine="+prismarine+" lamps="+lanterns+" water="+water);
int edges=0,corals=0;var touched=new HashSet<Long>();
for(int n=0;n<16;n++){
double angle=n*Math.PI/8;BlockPos shore=null;
for(int rad=440;rad>=280;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.water()&&WestOcean218.SEA-c.floor()>=12){shore=new BlockPos(x,c.floor(),z);break;}}
require(shore!=null,"Missing coastal shallows");var cp=new net.minecraft.world.level.ChunkPos(shore.getX()>>4,shore.getZ()>>4);var chunk=level.getChunk(cp.x(),cp.z());touched.add(cp.pack());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(!c.water())continue;edges++;
require(chunk.getBlockState(pos.set(x,WestOcean218.SEA,z)).getFluidState().is(net.minecraft.tags.FluidTags.WATER),"Broken water table");
require(chunk.getBlockState(pos.set(x,c.floor()-3,z)).isSolidRender(),"Open seabed");
for(int y=c.floor()+1;y<WestOcean218.SEA;y++){var s=chunk.getBlockState(pos.set(x,y,z));if(s.is(net.minecraft.tags.BlockTags.CORALS))corals++;}
for(var dir:Direction.Plane.HORIZONTAL){int nx=x+dir.getStepX(),nz=z+dir.getStepZ();var other=f.column(nx,nz);if(!other.water())require(level.getBlockState(pos.set(nx,WestOcean218.SEA,nz)).isSolidRender(),"Open shore");}
}
}
require(edges>600&&corals>0,"Missing coast/warm coral: "+edges+" / "+corals);
var details=WestFeatures218.plan(i);int palmLogs=0,spikeBlocks=0,cinnabar=0,lootChests=0,cliffChecks=0;
for(var palm:details.palms().stream().limit(8).toList()){
level.getChunkAt(palm);require(level.getBlockState(palm.above()).is(Blocks.JUNGLE_LOG),"Missing palm trunk");palmLogs++;
}
for(var source:details.geysers().stream().limit(4).toList()){
level.getChunkAt(source);require(level.getBlockState(source.below()).is(Blocks.POTENT_SULFUR)&&!level.getFluidState(source).isEmpty(),"Missing geothermal source");
}
var spikeSites=details.chunks().values().stream().flatMap(m->m.entrySet().stream()).filter(e->e.getValue().is(Blocks.SULFUR_SPIKE)).limit(24).toList();
for(var e:spikeSites){level.getChunkAt(e.getKey());require(level.getBlockState(e.getKey()).is(Blocks.SULFUR_SPIKE),"Missing sulfur spike");spikeBlocks++;}
for(var palm:details.geysers().stream().limit(4).toList())for(int dx=-8;dx<=8;dx++)for(int dz=-8;dz<=8;dz++){
int x=palm.getX()+dx,z=palm.getZ()+dz;var c=f.column(x,z);
for(int y=c.floor()-12;y<=c.floor();y++)if(level.getBlockState(pos.set(x,y,z)).is(Blocks.CINNABAR))cinnabar++;
}
for(var wreck:details.wrecks()){
// Native templates extend in both axes when rotated about their own pivot.
var centre=wreck.origin();
for(int x=centre.getX()-35;x<=centre.getX()+40;x++)for(int z=centre.getZ()-35;z<=centre.getZ()+40;z++)for(int y=centre.getY();y<=centre.getY()+24;y++){
if(level.getBlockState(pos.set(x,y,z)).is(Blocks.CHEST)&&level.getBlockEntity(pos) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)lootChests++;
}
}
// Inspect actual generated cliffs at twelve points, not just the shoreline water level.
for(int n=0;n<12;n++){
double angle=n*Math.PI/6;BlockPos cliff=null;
for(int rad=300;rad>0;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.floor()<180&&c.floor()>150){cliff=new BlockPos(x,c.floor(),z);break;}}
require(cliff!=null,"Missing cliff section");
for(int y=cliff.getY()+1;y<=WestOcean218.SEA;y++)for(var d:Direction.Plane.HORIZONTAL){
var q=cliff.relative(d).atY(y);var state=level.getBlockState(q);require(!state.isAir(),"Native underwater wall leaks at "+q);cliffChecks++;
}
}
require(cinnabar>0&&lootChests>=8,"Missing cinnabar/wreck loot "+cinnabar+"/"+lootChests);
r.result().put("coastalDetails",Map.of("palms",details.palms().size(),"palmsChecked",palmLogs,"spikes",details.spikes(),"spikeBlocksChecked",spikeBlocks,"geysers",details.geysers().size(),"cinnabarBlocksChecked",cinnabar,"wrecks",details.wrecks().size(),"lootChests",lootChests,"cliffSamples",cliffChecks));
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"goldBlocks",gold,"prismarine",prismarine,"lanterns",lanterns,"water",water));
r.result().put("ocean",Map.of("seaY",WestOcean218.SEA,"palms",WestFeatures218.plan(i).palms().size(),"coastalColumns",edges,"coastalChunks",touched.size(),"coralBlocks",corals));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(0,36,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("WEST218_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,118 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual anchor transaction and restart, followed by native monument and closed-water checks. */
final class West219Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(West219Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==219,"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","west219");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==6).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing West anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(6);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(WestOcean218.active(admitted)&&admitted.centerX()<0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong West placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=WestOcean218.field(i);var temple=WestTemple217.plan(i);
require(a.preparationChunks(i).size()<=110,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int gold=0,prismarine=0,lanterns=0,water=0;var b=temple.bounds();var pos=new BlockPos.MutableBlockPos();
for(int x=b.minX();x<=b.maxX();x++)for(int z=b.minZ();z<=b.maxZ();z++){
var c=f.column(x,z);require(level.getBlockState(pos.set(x,c.bottom()+8,z)).isSolidRender(),"Pierced foundation");
require(level.getBlockState(pos.set(x,WestOcean218.SEA,z)).is(Blocks.WATER),"Temple exposed");
for(int y=temple.origin().getY();y<=temple.origin().getY()+25;y++){
var s=level.getBlockState(pos.set(x,y,z));if(s.is(Blocks.GOLD_BLOCK))gold++;if(s.is(Blocks.PRISMARINE)||s.is(Blocks.PRISMARINE_BRICKS)||s.is(Blocks.DARK_PRISMARINE))prismarine++;if(s.is(Blocks.SEA_LANTERN))lanterns++;if(s.is(Blocks.WATER))water++;
}
}
require(gold==8&&prismarine>5000&&lanterns>10&&water>15000,"Native temple incomplete: gold="+gold+" prismarine="+prismarine+" lamps="+lanterns+" water="+water);
int edges=0,corals=0;var touched=new HashSet<Long>();
for(int n=0;n<16;n++){
double angle=n*Math.PI/8;BlockPos shore=null;
for(int rad=440;rad>=280;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.water()&&WestOcean218.SEA-c.floor()>=12){shore=new BlockPos(x,c.floor(),z);break;}}
require(shore!=null,"Missing coastal shallows");var cp=new net.minecraft.world.level.ChunkPos(shore.getX()>>4,shore.getZ()>>4);var chunk=level.getChunk(cp.x(),cp.z());touched.add(cp.pack());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(!c.water())continue;edges++;
require(chunk.getBlockState(pos.set(x,WestOcean218.SEA,z)).getFluidState().is(net.minecraft.tags.FluidTags.WATER),"Broken water table");
require(chunk.getBlockState(pos.set(x,c.floor()-3,z)).isSolidRender(),"Open seabed");
for(int y=c.floor()+1;y<WestOcean218.SEA;y++){var s=chunk.getBlockState(pos.set(x,y,z));if(s.is(net.minecraft.tags.BlockTags.CORALS))corals++;}
for(var dir:Direction.Plane.HORIZONTAL){int nx=x+dir.getStepX(),nz=z+dir.getStepZ();var other=f.column(nx,nz);if(!other.water())require(level.getBlockState(pos.set(nx,WestOcean218.SEA,nz)).isSolidRender(),"Open shore");}
}
}
require(edges>600&&corals>0,"Missing coast/warm coral: "+edges+" / "+corals);
var details=WestFeatures218.plan(i);int palmLogs=0,shoreColumns=0,lootChests=0,cliffChecks=0;
for(var palm:details.palms().stream().limit(8).toList()){
level.getChunkAt(palm);require(level.getBlockState(palm.above()).is(Blocks.JUNGLE_LOG),"Missing palm trunk");palmLogs++;
}
require(details.geysers().isEmpty()&&details.spikes()==0&&details.palms().size()>=350,"Old outer shore decorations remain");
for(int n=0;n<24;n++){
double angle=n*Math.PI/12;BlockPos edge=null;
for(int rad=280;rad<440;rad++){
int x=i.centerX()+(int)Math.round(Math.cos(angle)*rad),z=i.centerZ()+(int)Math.round(Math.sin(angle)*rad);var c=f.column(x,z);
if(c.shore()>=0&&c.shore()<4.5){edge=new BlockPos(x,c.floor(),z);break;}
}
require(edge!=null,"Missing low beach");var chunk=level.getChunkAt(edge);var cp=chunk.getPos();
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(c.water()||c.shore()>4.5)continue;shoreColumns++;
require(level.getBlockState(pos.set(x,220,z)).is(Blocks.SAND),"Raised/broken beach "+pos);
require(!level.getBlockState(pos.set(x,221,z)).is(Blocks.SAND),"Sand wall above water "+pos);
for(int y=202;y<=c.floor();y++){
var state=level.getBlockState(pos.set(x,y,z));require(!state.is(Blocks.SULFUR)&&!state.is(Blocks.CINNABAR),"Sulfur/cinnabar remains");
}
}
}
for(var wreck:details.wrecks()){
// Native templates extend in both axes when rotated about their own pivot.
var centre=wreck.origin();
for(int x=centre.getX()-35;x<=centre.getX()+40;x++)for(int z=centre.getZ()-35;z<=centre.getZ()+40;z++)for(int y=centre.getY();y<=centre.getY()+24;y++){
if(level.getBlockState(pos.set(x,y,z)).is(Blocks.CHEST)&&level.getBlockEntity(pos) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)lootChests++;
}
}
// Inspect actual generated cliffs at twelve points, not just the shoreline water level.
for(int n=0;n<12;n++){
double angle=n*Math.PI/6;BlockPos cliff=null;
for(int rad=300;rad>0;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.floor()<180&&c.floor()>150){cliff=new BlockPos(x,c.floor(),z);break;}}
require(cliff!=null,"Missing cliff section");
for(int y=cliff.getY()+1;y<=WestOcean218.SEA;y++)for(var d:Direction.Plane.HORIZONTAL){
var q=cliff.relative(d).atY(y);var state=level.getBlockState(q);require(!state.isAir(),"Native underwater wall leaks at "+q);cliffChecks++;
}
}
require(shoreColumns>300&&lootChests>=8,"Missing low beach/wreck loot "+shoreColumns+"/"+lootChests);
r.result().put("coastalDetails",Map.of("palms",details.palms().size(),"palmsChecked",palmLogs,"lowBeachColumnsChecked",shoreColumns,"geysers",details.geysers().size(),"wrecks",details.wrecks().size(),"lootChests",lootChests,"cliffSamples",cliffChecks));
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"goldBlocks",gold,"prismarine",prismarine,"lanterns",lanterns,"water",water));
r.result().put("ocean",Map.of("seaY",WestOcean218.SEA,"palms",WestFeatures218.plan(i).palms().size(),"coastalColumns",edges,"coastalChunks",touched.size(),"coralBlocks",corals));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(0,36,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("WEST219_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,125 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual anchor transaction and restart, followed by native monument and closed-water checks. */
final class West220Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(West220Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==220,"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","west220");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==6).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing West anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(6);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(WestOcean218.active(admitted)&&admitted.centerX()<0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong West placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=WestOcean218.field(i);var temple=WestTemple217.plan(i);
require(a.preparationChunks(i).size()<=110,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int gold=0,prismarine=0,lanterns=0,water=0;var b=temple.bounds();var pos=new BlockPos.MutableBlockPos();
for(int x=b.minX();x<=b.maxX();x++)for(int z=b.minZ();z<=b.maxZ();z++){
var c=f.column(x,z);require(level.getBlockState(pos.set(x,c.bottom()+8,z)).isSolidRender(),"Pierced foundation");
require(level.getBlockState(pos.set(x,WestOcean218.SEA,z)).is(Blocks.WATER),"Temple exposed");
for(int y=temple.origin().getY();y<=temple.origin().getY()+25;y++){
var s=level.getBlockState(pos.set(x,y,z));if(s.is(Blocks.GOLD_BLOCK))gold++;if(s.is(Blocks.PRISMARINE)||s.is(Blocks.PRISMARINE_BRICKS)||s.is(Blocks.DARK_PRISMARINE))prismarine++;if(s.is(Blocks.SEA_LANTERN))lanterns++;if(s.is(Blocks.WATER))water++;
}
}
require(gold==8&&prismarine>5000&&lanterns>10&&water>15000,"Native temple incomplete: gold="+gold+" prismarine="+prismarine+" lamps="+lanterns+" water="+water);
int edges=0,corals=0;var touched=new HashSet<Long>();
for(int n=0;n<16;n++){
double angle=n*Math.PI/8;BlockPos shore=null;
for(int rad=440;rad>=280;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.water()&&WestOcean218.SEA-c.floor()>=12){shore=new BlockPos(x,c.floor(),z);break;}}
require(shore!=null,"Missing coastal shallows");var cp=new net.minecraft.world.level.ChunkPos(shore.getX()>>4,shore.getZ()>>4);var chunk=level.getChunk(cp.x(),cp.z());touched.add(cp.pack());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(!c.water())continue;edges++;
require(chunk.getBlockState(pos.set(x,WestOcean218.SEA,z)).getFluidState().is(net.minecraft.tags.FluidTags.WATER),"Broken water table");
require(chunk.getBlockState(pos.set(x,c.floor()-3,z)).isSolidRender(),"Open seabed");
for(int y=c.floor()+1;y<WestOcean218.SEA;y++){var s=chunk.getBlockState(pos.set(x,y,z));if(s.is(net.minecraft.tags.BlockTags.CORALS))corals++;}
for(var dir:Direction.Plane.HORIZONTAL){int nx=x+dir.getStepX(),nz=z+dir.getStepZ();var other=f.column(nx,nz);if(!other.water())require(level.getBlockState(pos.set(nx,WestOcean218.SEA,nz)).isSolidRender(),"Open shore");}
}
}
require(edges>600&&corals>0,"Missing coast/warm coral: "+edges+" / "+corals);
var details=WestFeatures218.plan(i);int palmLogs=0,shoreColumns=0,lootChests=0,cliffChecks=0;
for(var palm:details.palms().stream().limit(8).toList()){
level.getChunkAt(palm);require(level.getBlockState(palm.above()).is(Blocks.JUNGLE_LOG),"Missing palm trunk");palmLogs++;
}
require(details.geysers().isEmpty()&&details.spikes()==0&&details.palms().size()>=60,"Old outer shore decorations remain");
for(int n=0;n<24;n++){
double angle=n*Math.PI/12;BlockPos edge=null;
for(int rad=280;rad<440;rad++){
int x=i.centerX()+(int)Math.round(Math.cos(angle)*rad),z=i.centerZ()+(int)Math.round(Math.sin(angle)*rad);var c=f.column(x,z);
if(c.shore()>=0&&c.shore()<12){edge=new BlockPos(x,c.floor(),z);break;}
}
require(edge!=null,"Missing low beach");var chunk=level.getChunkAt(edge);var cp=chunk.getPos();
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(c.water()||c.shore()>12)continue;shoreColumns++;
require(level.getBlockState(pos.set(x,220,z)).is(Blocks.SAND),"Raised/broken beach "+pos);
require(level.getBlockState(pos.set(x,221,z)).isAir(),"Sand wall above water "+pos);
for(int y=202;y<=c.floor();y++){
var state=level.getBlockState(pos.set(x,y,z));require(!state.is(Blocks.SULFUR)&&!state.is(Blocks.CINNABAR),"Sulfur/cinnabar remains");
}
}
}
int plantsChecked=0;
for(var block:List.of(Blocks.CACTUS,Blocks.SHORT_DRY_GRASS,Blocks.TALL_DRY_GRASS,Blocks.DEAD_BUSH,Blocks.OAK_LOG,Blocks.STRIPPED_OAK_LOG,Blocks.DANDELION)){
var samples=details.chunks().values().stream().flatMap(m->m.entrySet().stream()).filter(e->e.getValue().is(block)).limit(3).toList();
require(!samples.isEmpty(),"Missing vegetation category "+block);
for(var e:samples){level.getChunkAt(e.getKey());var state=level.getBlockState(e.getKey());require(state.is(block),"Missing native vegetation "+e.getKey()+" expected="+block+" actual="+state);require(state.canSurvive(level,e.getKey()),"Unstable native vegetation "+e.getKey());plantsChecked++;}
}
r.result().put("vegetationChecked",plantsChecked);
for(var wreck:details.wrecks()){
// Native templates extend in both axes when rotated about their own pivot.
var centre=wreck.origin();
for(int x=centre.getX()-35;x<=centre.getX()+40;x++)for(int z=centre.getZ()-35;z<=centre.getZ()+40;z++)for(int y=centre.getY();y<=centre.getY()+24;y++){
if(level.getBlockState(pos.set(x,y,z)).is(Blocks.CHEST)&&level.getBlockEntity(pos) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)lootChests++;
}
}
// Inspect actual generated cliffs at twelve points, not just the shoreline water level.
for(int n=0;n<12;n++){
double angle=n*Math.PI/6;BlockPos cliff=null;
for(int rad=300;rad>0;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.floor()<180&&c.floor()>150){cliff=new BlockPos(x,c.floor(),z);break;}}
require(cliff!=null,"Missing cliff section");
for(int y=cliff.getY()+1;y<=WestOcean218.SEA;y++)for(var d:Direction.Plane.HORIZONTAL){
var q=cliff.relative(d).atY(y);var state=level.getBlockState(q);require(!state.isAir(),"Native underwater wall leaks at "+q);cliffChecks++;
}
}
require(shoreColumns>300&&lootChests>=8,"Missing low beach/wreck loot "+shoreColumns+"/"+lootChests);
r.result().put("coastalDetails",Map.of("palms",details.palms().size(),"palmsChecked",palmLogs,"lowBeachColumnsChecked",shoreColumns,"geysers",details.geysers().size(),"wrecks",details.wrecks().size(),"lootChests",lootChests,"cliffSamples",cliffChecks));
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"goldBlocks",gold,"prismarine",prismarine,"lanterns",lanterns,"water",water));
r.result().put("ocean",Map.of("seaY",WestOcean218.SEA,"palms",WestFeatures218.plan(i).palms().size(),"coastalColumns",edges,"coastalChunks",touched.size(),"coralBlocks",corals));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(0,36,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("WEST220_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,131 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
/** Actual anchor transaction and restart, followed by native monument and closed-water checks. */
final class West221Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
static void register(){ServerTickEvents.END_SERVER_TICK.register(West221Checks::tick);}
static void begin(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh){
var level=server.overworld();var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
require(adapter!=null&&adapter.rootGeneration()==221,"Wrong profile");var session=ExpansionRuntime.session(level);result.put("verificationScope","west221");
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==6).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing West anchor");
var core=level.getBlockState(AnchorOffers189.CORE);require(core.is(OriginBlock.BLOCK),"Missing core");level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);require(RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()&&wrong.getCount()==64,"Wrong offer accepted");
var demand=AnchorOffers189.DEMANDS.get(6);var payment=new ItemStack(demand.item(),demand.count()+1);var request=RelayOffers199.offer(level,pos,payment,()->true);
require(RelayOffers199.offer(level,pos,payment,()->true).isCompletedExceptionally(),"Duplicate accepted");result.put("anchor",pos.toShortString());
run=new Run(server,output,result,started,true,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(1)),ItemStack.EMPTY);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
require(System.nanoTime()-r.started()<java.util.concurrent.TimeUnit.MINUTES.toNanos(8),"Timeout");if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
require(WestOcean218.active(admitted)&&admitted.centerX()<0&&Math.abs(admitted.centerZ())/Math.abs((double)admitted.centerX())<.28,"Wrong West placement");
r.result().put("expansion",Map.of("id",admitted.id(),"x",admitted.centerX(),"z",admitted.centerZ(),"diameter",admitted.diameter(),"seed",admitted.seed()));
if(r.fresh()){require(r.payment().getCount()==1&&session.island(admitted.id()).status().equals("reserved"),"Payment/interruption failed");r.result().put("interruptedProgress",session.progress());finish(r);return;}
var i=session.island(admitted.id());if(!i.status().equals("ready")){require(!session.progress().contains("Exception")&&!session.progress().equals("idle"),"Preparation failed: "+session.progress());return;}
var a=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=RegionalRelay215.plan(i);var f=WestOcean218.field(i);var temple=WestTemple217.plan(i);
require(a.preparationChunks(i).size()<=110,"Unbounded preparation");int relayBlocks=0;
for(var part:relay.chunks().values())for(var entry:part.entrySet()){require(level.getBlockState(entry.getKey())==entry.getValue(),"Relay mismatch "+entry.getKey());relayBlocks++;}
try{RelayOffers199.offer(level,relay.anchor(),new ItemStack(Items.DIRT),()->true).join();throw new AssertionError("Bad relay payment");}catch(java.util.concurrent.CompletionException expected){require("required".equals(expected.getCause().getMessage()),"Relay unrecognized");}
int gold=0,prismarine=0,lanterns=0,water=0;var b=temple.bounds();var pos=new BlockPos.MutableBlockPos();
for(int x=b.minX();x<=b.maxX();x++)for(int z=b.minZ();z<=b.maxZ();z++){
var c=f.column(x,z);require(level.getBlockState(pos.set(x,c.bottom()+8,z)).isSolidRender(),"Pierced foundation");
require(level.getBlockState(pos.set(x,WestOcean218.SEA,z)).is(Blocks.WATER),"Temple exposed");
for(int y=temple.origin().getY();y<=temple.origin().getY()+25;y++){
var s=level.getBlockState(pos.set(x,y,z));if(s.is(Blocks.GOLD_BLOCK))gold++;if(s.is(Blocks.PRISMARINE)||s.is(Blocks.PRISMARINE_BRICKS)||s.is(Blocks.DARK_PRISMARINE))prismarine++;if(s.is(Blocks.SEA_LANTERN))lanterns++;if(s.is(Blocks.WATER))water++;
}
}
require(gold==8&&prismarine>5000&&lanterns>10&&water>15000,"Native temple incomplete: gold="+gold+" prismarine="+prismarine+" lamps="+lanterns+" water="+water);
int edges=0,corals=0;var touched=new HashSet<Long>();
for(int n=0;n<16;n++){
double angle=n*Math.PI/8;BlockPos shore=null;
for(int rad=440;rad>=280;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.water()&&WestOcean218.SEA-c.floor()>=12){shore=new BlockPos(x,c.floor(),z);break;}}
require(shore!=null,"Missing coastal shallows");var cp=new net.minecraft.world.level.ChunkPos(shore.getX()>>4,shore.getZ()>>4);var chunk=level.getChunk(cp.x(),cp.z());touched.add(cp.pack());
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(!c.water())continue;edges++;
require(chunk.getBlockState(pos.set(x,WestOcean218.SEA,z)).getFluidState().is(net.minecraft.tags.FluidTags.WATER),"Broken water table");
require(chunk.getBlockState(pos.set(x,c.floor()-3,z)).isSolidRender(),"Open seabed");
for(int y=c.floor()+1;y<WestOcean218.SEA;y++){var s=chunk.getBlockState(pos.set(x,y,z));if(s.is(net.minecraft.tags.BlockTags.CORALS))corals++;}
for(var dir:Direction.Plane.HORIZONTAL){int nx=x+dir.getStepX(),nz=z+dir.getStepZ();var other=f.column(nx,nz);if(!other.water())require(level.getBlockState(pos.set(nx,WestOcean218.SEA,nz)).isSolidRender(),"Open shore");}
}
}
require(edges>600&&corals>0,"Missing coast/warm coral: "+edges+" / "+corals);
var details=WestFeatures218.plan(i);int palmLogs=0,shoreColumns=0,lootChests=0,cliffChecks=0;
for(var palm:details.palms().stream().limit(8).toList()){
level.getChunkAt(palm);require(level.getBlockState(palm.above()).is(Blocks.JUNGLE_LOG),"Missing palm trunk");palmLogs++;
}
require(details.geysers().isEmpty()&&details.spikes()==0&&details.palms().size()>=60,"Old outer shore decorations remain");
for(int n=0;n<24;n++){
double angle=n*Math.PI/12;BlockPos edge=null;
for(int rad=280;rad<440;rad++){
int x=i.centerX()+(int)Math.round(Math.cos(angle)*rad),z=i.centerZ()+(int)Math.round(Math.sin(angle)*rad);var c=f.column(x,z);
if(c.shore()>=0&&c.shore()<12){edge=new BlockPos(x,c.floor(),z);break;}
}
require(edge!=null,"Missing low beach");var chunk=level.getChunkAt(edge);var cp=chunk.getPos();
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++){
var c=f.column(x,z);if(c.water()||c.shore()>12)continue;shoreColumns++;
require(level.getBlockState(pos.set(x,220,z)).is(Blocks.SAND),"Raised/broken beach "+pos);
require(level.getBlockState(pos.set(x,221,z)).isAir(),"Sand wall above water "+pos);
for(int y=202;y<=c.floor();y++){
var state=level.getBlockState(pos.set(x,y,z));require(!state.is(Blocks.SULFUR)&&!state.is(Blocks.CINNABAR),"Sulfur/cinnabar remains");
}
}
}
int plantsChecked=0;
for(var block:List.of(Blocks.CACTUS,Blocks.SHORT_DRY_GRASS,Blocks.TALL_DRY_GRASS,Blocks.DEAD_BUSH,Blocks.OAK_LOG,Blocks.DANDELION)){
var samples=details.chunks().values().stream().flatMap(m->m.entrySet().stream()).filter(e->e.getValue().is(block)).limit(3).toList();
require(!samples.isEmpty(),"Missing vegetation category "+block);
for(var e:samples){level.getChunkAt(e.getKey());var state=level.getBlockState(e.getKey());require(state.is(block),"Missing native vegetation "+e.getKey()+" expected="+block+" actual="+state);require(state.canSurvive(level,e.getKey()),"Unstable native vegetation "+e.getKey());plantsChecked++;}
}
r.result().put("vegetationChecked",plantsChecked);
require(details.chunks().values().stream().flatMap(m->m.values().stream()).noneMatch(s->s.is(Blocks.STRIPPED_OAK_LOG)),"Stripped trees remain");
int yellowRock=0;
for(var palm:details.palms().stream().limit(8).toList())for(int y=palm.getY()-12;y<palm.getY()-4;y++){
var p=palm.atY(y);if(level.getBlockState(p).is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellowRock++;
}
require(yellowRock>30,"New rock absent in actual terrain");r.result().put("yellowRockChecked",yellowRock);
for(var wreck:details.wrecks()){
// Native templates extend in both axes when rotated about their own pivot.
var centre=wreck.origin();
for(int x=centre.getX()-35;x<=centre.getX()+40;x++)for(int z=centre.getZ()-35;z<=centre.getZ()+40;z++)for(int y=centre.getY();y<=centre.getY()+24;y++){
if(level.getBlockState(pos.set(x,y,z)).is(Blocks.CHEST)&&level.getBlockEntity(pos) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity chest&&chest.getLootTable()!=null)lootChests++;
}
}
// Inspect actual generated cliffs at twelve points, not just the shoreline water level.
for(int n=0;n<12;n++){
double angle=n*Math.PI/6;BlockPos cliff=null;
for(int rad=300;rad>0;rad--){int x=i.centerX()+(int)(Math.cos(angle)*rad),z=i.centerZ()+(int)(Math.sin(angle)*rad);var c=f.column(x,z);if(c.floor()<180&&c.floor()>150){cliff=new BlockPos(x,c.floor(),z);break;}}
require(cliff!=null,"Missing cliff section");
for(int y=cliff.getY()+1;y<=WestOcean218.SEA;y++)for(var d:Direction.Plane.HORIZONTAL){
var q=cliff.relative(d).atY(y);var state=level.getBlockState(q);require(!state.isAir(),"Native underwater wall leaks at "+q);cliffChecks++;
}
}
require(shoreColumns>300&&lootChests>=8,"Missing low beach/wreck loot "+shoreColumns+"/"+lootChests);
r.result().put("coastalDetails",Map.of("palms",details.palms().size(),"palmsChecked",palmLogs,"lowBeachColumnsChecked",shoreColumns,"geysers",details.geysers().size(),"wrecks",details.wrecks().size(),"lootChests",lootChests,"cliffSamples",cliffChecks));
r.result().put("temple",Map.of("origin",temple.origin().toShortString(),"goldBlocks",gold,"prismarine",prismarine,"lanterns",lanterns,"water",water));
r.result().put("ocean",Map.of("seaY",WestOcean218.SEA,"palms",WestFeatures218.plan(i).palms().size(),"coastalColumns",edges,"coastalChunks",touched.size(),"coralBlocks",corals));
r.result().put("relay",Map.of("position",relay.anchor().toShortString(),"checkedBlocks",relayBlocks,"preparedChunks",a.preparationChunks(i).size()));
var visit=new LinkedHashMap<String,Object>();visit.put("entrance",f.arrival().toShortString());visit.put("camera",f.arrival().offset(0,36,0).toShortString());visit.put("temple",temple.origin().toShortString());visit.put("relay",relay.anchor().toShortString());r.result().put("visit",visit);finish(r);
}catch(Throwable e){run=null;fr.koka.sanctuary.SanctuaryMod.LOGGER.error("WEST221_CHECK_FAILED",e);server.halt(false);}
}
private static void finish(Run r){run=null;r.result().put("readyForVisitMs",(System.nanoTime()-r.started())/1_000_000);WorldgenDiagnostics.writeReport(r.output(),r.result());}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,162 @@
package fr.koka.sanctuarytest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.expansion.ExpansionIsland;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import fr.koka.sanctuary.worldgen.SanctuaryBiomeSource;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.*;
import java.util.*;
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.levelgen.*;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.storage.LevelResource;
import static fr.koka.sanctuarytest.WorldgenLab.*;
/** Opt-in measurements; never loaded by the production mod. */
final class WorldgenDiagnostics {
static void register() {
Expansion199Checks.register();
North200Checks.register();
South209Checks.register();
South210Checks.register();
South211Checks.register();
South212Checks.register();
South213Checks.register();
South214Checks.register();
Relays215Checks.register();West217Checks.register();West218Checks.register();West219Checks.register();West220Checks.register();West221Checks.register();East223Checks.register();Diagonal225Checks.register();Diagonal226Checks.register();Diagonal227Checks.register();Diagonal228Checks.register();Diagonal229Checks.register();Diagonal230Checks.register();Diagonal231Checks.register();AllIslands231.register();
ServerTickEvents.END_SERVER_TICK.register(EcologySurvey176::tick);
WorldgenLab.diagnostics = WorldgenDiagnostics::report;
}
private static void report(MinecraftServer server, Run run) {
var level = server.overworld();
var folder = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
if (!folder.getFileName().toString().equals("worldgen-lab-v1"))
throw new IllegalStateException("Worldgen metrics require the dedicated worldgen-lab-v1 directory");
boolean fast = relief(level);
if (!fast && !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator))
throw new IllegalStateException("Unrecognized worldgen laboratory profile");
var result = new LinkedHashMap<String, Object>();
boolean ecology=ecology(level.getChunkSource().getGenerator());
boolean sky = fast && (((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).generatorSettings().is(SKY_SETTINGS) || ecology);
result.put("profile", coherent193(level.getChunkSource().getGenerator()) ? "coherent-landforms-v1" : eroded192(level.getChunkSource().getGenerator()) ? "eroded-massif-v1" : original191(level.getChunkSource().getGenerator()) ? "pre-terraces-v1" : refined190(level.getChunkSource().getGenerator()) ? "natural-refinement-v1" : finish189(level.getChunkSource().getGenerator()) ? "terrain-finish-v1" : details188(level.getChunkSource().getGenerator()) ? "living-details-v1" : natural187(level.getChunkSource().getGenerator()) ? "natural-caves-v1" : anchors186(level.getChunkSource().getGenerator()) ? "hidden-anchors-v1" : stem185(level.getChunkSource().getGenerator()) ? "fine-stem-v1" : ascent184(level.getChunkSource().getGenerator()) ? "ascent-v1" : discovery183(level.getChunkSource().getGenerator()) ? "discovery-v1" : gallery182(level.getChunkSource().getGenerator()) ? "gallery-v1" : origin181(level.getChunkSource().getGenerator()) ? "origin-v1" : ecology180(level.getChunkSource().getGenerator()) ? "adventure-ecology-v1" : ecology179(level.getChunkSource().getGenerator()) ? "living-ecology-v1" : ecology178(level.getChunkSource().getGenerator()) ? "rocky-ecology-v1" : ecology177(level.getChunkSource().getGenerator()) ? "relief-ecology-v1" : ecology ? "ecology-v1" : sky ? "sky-v1" : fast ? "relief-v1" : "reference");
var sharedSize=SharedIsland196.size(level.getChunkSource().getGenerator());
if(sharedSize!=null){result.put("sharedSize",sharedSize.name());result.put("diameter",sharedSize.diameter);result.put("terrainLimit",sharedSize.capacity.terrainLimit());}
result.put("seed", level.getSeed()); result.put("fresh", run.fresh());
result.put("serverStartedMs", (System.nanoTime() - run.started()) / 1_000_000);
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal227")){Diagonal227Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal228")){Diagonal228Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal229")){Diagonal229Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal231")){Diagonal231Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal230")){Diagonal230Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal226")){Diagonal226Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.diagonal225")){Diagonal225Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.west219")){West219Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.west220")){West220Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&(Boolean.getBoolean("sanctuary.worldgenLab.east223")||Boolean.getBoolean("sanctuary.worldgenLab.east224"))){East223Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.west221")){West221Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.west218")){West218Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.west217")){West217Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&(Boolean.getBoolean("sanctuary.worldgenLab.south216")||Boolean.getBoolean("sanctuary.worldgenLab.south215")||Boolean.getBoolean("sanctuary.worldgenLab.north215"))){
Relays215Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south214")){
South214Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south213")){
South213Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south212")){
South212Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south211")){
South211Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south210")){
South210Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.south209")){
South209Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
}
if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&(Boolean.getBoolean("sanctuary.worldgenLab.north200")||Boolean.getBoolean("sanctuary.worldgenLab.north201")||Boolean.getBoolean("sanctuary.worldgenLab.north202")||Boolean.getBoolean("sanctuary.worldgenLab.north208")||Boolean.getBoolean("sanctuary.worldgenLab.north207")||Boolean.getBoolean("sanctuary.worldgenLab.north206")||Boolean.getBoolean("sanctuary.worldgenLab.north205")||Boolean.getBoolean("sanctuary.worldgenLab.north204")||Boolean.getBoolean("sanctuary.worldgenLab.north203"))){
North200Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
} else if(Boolean.getBoolean("sanctuary.worldgenLab.verify")&&Boolean.getBoolean("sanctuary.worldgenLab.expansion199")){
Expansion199Checks.begin(server,folder.getParent(),result,run.started(),run.fresh());return;
} else if (Boolean.getBoolean("sanctuary.worldgenLab.verify") && Boolean.getBoolean("sanctuary.worldgenLab.gallery198")) {
if(SharedIsland196.revision(level.getChunkSource().getGenerator())!=198)throw new IllegalStateException("Gallery regression requires a beta.198 profile");
var source=(AdventureBiomes180)level.getChunkSource().getGenerator().getBiomeSource();
result.put("verificationScope","gallery198");
result.put("gallery198Matrix",Gallery198Checks.check(level));
result.put("gallery",Cavern183.check(level));
result.put("ornaments",Gallery189.check(level,source));
} else if (Boolean.getBoolean("sanctuary.worldgenLab.verify")) {
result.put("verificationScope","full");
long start = System.nanoTime();
int[][] chunks = {{0,0},{1,0},{-1,0},{0,1},{0,-1},{12,0},{-12,0},{0,12},{0,-12},{24,0}};
for (int[] chunk : chunks) level.getChunk(chunk[0], chunk[1]);
result.put("probeChunks", chunks); result.put("probeChunksMs", (System.nanoTime()-start)/1_000_000);
if (fast) {
var noise = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
var random = level.getChunkSource().randomState();
var expected = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("unified_"+(SharedIsland196.size(noise)==null?10:SharedIsland196.size(noise).capacity.players())))).value();
var reference = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), level.getSeed(), expected);
int samples = 0;
for (int x = -384; x <= 384; x += 96) for (int z = -384; z <= 384; z += 96)
for (int y = 16; y < (ecology177(noise) || ecology178(noise) || ecology179(noise) || ecology180(noise) ? 216 : sky ? 300 : 640); y += 32) {
double actual = random.sampleBlockValueUncached(noise.generatorSettings().value().noiseRouter().finalDensity(), x,y,z);
double wanted = reference.sampleBlockValueUncached(expected.noiseRouter().finalDensity(), x,y,z);
if (Double.doubleToLongBits(actual) != Double.doubleToLongBits(wanted))
throw new IllegalStateException("Relief diverged from Sanctuary at " + new BlockPos(x,y,z));
samples++;
}
int plans = PopulationHydrologyRuntime.cachedRegionCount(random);
if (plans != 0) throw new IllegalStateException("Terrain preview computed hydrology");
if (Files.exists(folder.resolve("data/sanctuary-world-v24")))
throw new IllegalStateException("Terrain preview created an expansion journal");
result.put("matchingDensitySamples", samples); result.put("hydrologyRegions", plans);
if(ecology180(noise)) {
result.put("quickChecks",AdventureChecks180.check(level));
if(SharedIsland196.current(noise))result.put("details197",Details197.check(level));
if(SharedIsland196.revision(noise)==198)result.put("gallery198Matrix",Gallery198Checks.check(level));
if(finishedTerrain(noise))result.put("finishChecks",Ascent189.check(level));
else if(details188(noise))result.put("detailChecks",Ascent188.check(level));
else if(natural187(noise))result.put("naturalChecks",Ascent187.check(level));
else if(anchors186(noise))result.put("anchorChecks",Ascent186.check(level));
if(stem185(noise))result.put("fineStemChecks",Ascent185.check(level));
if(ascent184(noise))result.put("ascentChecks",Ascent184.check(level));
if(discovery183(noise))result.put("discoveryChecks",Discovery183.check(level));
if(origin181(noise))result.put("originChecks",Origin181.check(level));
if(gallery182(noise)){result.put("galleryChecks",Cavern182.check(level));result.put("rosetteChecks",Rosette182.check(level));}
} else if(ecology179(noise)) {
result.put("quickChecks",LivingChecks179.check(level));
} else if(ecology178(noise)) {
result.put("quickChecks",RockyChecks178.check(level));
} else if(ecology) {
EcologySurvey176.begin(server,folder.getParent(),result,run.started(),run.fresh());
return;
}
if (sky && !ecology178(noise) && !ecology179(noise) && !ecology180(noise)) result.put("survey", SkySurvey175.measure(level, folder.getParent(), reference, expected, run.fresh()));
}
}
result.put("readyForVisitMs", (System.nanoTime() - run.started()) / 1_000_000);
writeReport(folder.getParent(),result);
}
static void writeReport(Path directory,Map<String,Object> result) {
try {
Files.writeString(directory.resolve("worldgen-lab-metrics.json"),
new GsonBuilder().setPrettyPrinting().create().toJson(result) + "\n");
} catch (java.io.IOException error) { throw new IllegalStateException("Cannot write worldgen metrics", error); }
SanctuaryMod.LOGGER.info("WORLDGEN_LAB_READY {}", result);
}
}
@@ -1,68 +0,0 @@
package fr.koka.sanctuarytest.mixin;
import fr.koka.sanctuarytest.LabMinerals175;
import fr.koka.sanctuarytest.EcologyWater176;
import net.minecraft.world.level.*;
import net.minecraft.world.level.chunk.*;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ChunkGenerator.class)
public abstract class LabDecorationMixin {
/** Future native biomes must not opt this lab into vanilla stronghold/village placement. */
@Inject(method="createState",at=@At("HEAD"),cancellable=true)
private void sanctuaryTest$structures199(net.minecraft.core.HolderLookup<net.minecraft.world.level.levelgen.structure.StructureSet> structures,
net.minecraft.world.level.levelgen.RandomState random,long seed,
org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable<ChunkGeneratorStructureState> ci){
var generator=(ChunkGenerator)(Object)this;
if(fr.koka.sanctuarytest.SharedIsland196.revision(generator)>=199)
ci.setReturnValue(ChunkGeneratorStructureState.createForFlat(random,seed,generator.getOrigin(random),generator.getBiomeSource(),java.util.stream.Stream.empty()));
}
@Inject(method="applyBiomeDecoration",at=@At("HEAD"))
private void sanctuaryTest$localWater(WorldGenLevel level,ChunkAccess chunk,StructureManager structures,CallbackInfo callback) {
if(fr.koka.sanctuarytest.LabExpansions199.owns((ChunkGenerator)(Object)this,chunk.getPos()))return;
fr.koka.sanctuarytest.Terrain189.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Water189.prepare(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Lakes188.prepare(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Dungeon180.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Basins180.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Cherry180.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.AdventureCaves180.groves(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.CaveComplex179.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.LivingCaves179.groves(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.LivingCaves179.prepare(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.ReliefGroves177.place(level,(ChunkGenerator)(Object)this,chunk);
EcologyWater176.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.RockyWater178.place(level,(ChunkGenerator)(Object)this,chunk);
}
@Inject(method="applyBiomeDecoration",at=@At("TAIL"))
private void sanctuaryTest$labDeposits(WorldGenLevel level,ChunkAccess chunk,StructureManager structures,CallbackInfo callback) {
if(fr.koka.sanctuarytest.LabExpansions199.owns((ChunkGenerator)(Object)this,chunk.getPos())){
var adapter=fr.koka.sanctuarytest.LabExpansions199.get((ChunkGenerator)(Object)this);
var island=adapter.regionAt(chunk.getPos().getMiddleBlockX(),chunk.getPos().getMiddleBlockZ());
if(fr.koka.sanctuary.expansion.NorthTerrain202.active(island))fr.koka.sanctuary.expansion.NorthTerrain202.snow(island,chunk);
fr.koka.sanctuarytest.NorthVillage200.place(level,(ChunkGenerator)(Object)this,chunk);return;
}
fr.koka.sanctuarytest.LivingCaves179.decorate(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.AdventureCaves180.decorate(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Dungeon180.populate(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Discovery183.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent184.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent185.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent186.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent187.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent188.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Ascent189.place(level,(ChunkGenerator)(Object)this,chunk);
LabMinerals175.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Gems183.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Cavern182.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Rosette182.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Origin181.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Anchors186.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Anchors187.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Anchors188.place(level,(ChunkGenerator)(Object)this,chunk);
fr.koka.sanctuarytest.Anchors189.place(level,(ChunkGenerator)(Object)this,chunk);
}
}
@@ -1,22 +0,0 @@
package fr.koka.sanctuarytest.mixin;
import fr.koka.sanctuarytest.*;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.*;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Reject the entire feature before its roots, trunk or canopy are written. */
@Mixin({TreeFeature.class,RootSystemFeature.class})
public abstract class TreeGround189Mixin {
@Inject(method="place",at=@At("HEAD"),cancellable=true)
private void sanctuaryTest$dryGround(WorldGenLevel level,ChunkGenerator generator,RandomSource random,BlockPos origin,CallbackInfoReturnable<Boolean> ci){
if(NorthVillage200.rejectTree(generator,origin)){ci.setReturnValue(false);return;}
if(LabExpansions199.owns(generator,new net.minecraft.world.level.ChunkPos(origin.getX()>>4,origin.getZ()>>4)))return;
if(Water189.rejectTree(generator,origin,(Object)this instanceof RootSystemFeature))ci.setReturnValue(false);
}
}
@@ -1,4 +0,0 @@
{
"replace": false,
"values": []
}
@@ -2,15 +2,26 @@
"schemaVersion": 1,
"id": "sanctuary_test",
"version": "${version}",
"name": "Sanctuary — Quick Test",
"name": "Sanctuary \u2014 Quick Test",
"description": "Optional development labs: flat worlds, fast Sanctuary terrain and local validation tools.",
"authors": ["KOKA99CAB"],
"authors": [
"KOKA99CAB"
],
"license": "GPL-3.0-or-later",
"environment": "*",
"mixins": ["sanctuary-test.mixins.json"],
"entrypoints": {
"main": ["fr.koka.sanctuarytest.QuickTestMod"],
"client": ["fr.koka.sanctuarytest.QuickTestClient"]
"main": [
"fr.koka.sanctuarytest.QuickTestMod"
],
"client": [
"fr.koka.sanctuarytest.QuickTestClient"
]
},
"depends": {"fabricloader": ">=${loader}", "minecraft": "${minecraft}", "java": ">=25", "fabric-api": "*", "sanctuary": "${version}"}
"depends": {
"fabricloader": ">=${loader}",
"minecraft": "${minecraft}",
"java": ">=25",
"fabric-api": "*",
"sanctuary": "${version}"
}
}
+34 -3
View File
@@ -228,9 +228,12 @@ tasks.named('processGametestResources') {
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
def focusClasses = [palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
def focusClasses = [solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
inputs.property('focusedNative', focused)
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('yellowRock222Client', yellowRock222Client)
if (yellowRock222Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.YellowRock222ClientChecks') }
def introClient = providers.gradleProperty('sanctuaryIntroClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('introClient', introClient)
if (introClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Intro031ClientChecks') }
@@ -328,6 +331,12 @@ tasks.named('processGametestResources') {
def weather082Client = providers.gradleProperty('sanctuaryWeather082ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryWeather082ClientTests', weather082Client)
if (weather082Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Weather082ClientChecks') }
def solo232Client = providers.gradleProperty('sanctuarySolo232ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySolo232ClientTests', solo232Client)
if (solo232Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Solo232ClientChecks') }
def pause233Client = providers.gradleProperty('sanctuaryPause233ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryPause233ClientTests', pause233Client)
if (pause233Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pause233ClientChecks') }
def balance080Client = providers.gradleProperty('sanctuaryBalance080ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryBalance080ClientTests', balance080Client)
if (balance080Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Balance080ClientChecks') }
@@ -555,8 +564,6 @@ dependencies {
include('org.mariadb.jdbc:mariadb-java-client:3.5.10')
implementation project(':jei')
include project(':jei')
implementation project(':demeure')
include project(':demeure')
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}"
@@ -1353,3 +1360,27 @@ tasks.register('north203Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.expansion.North203Smoke'
}
tasks.named('check') { dependsOn 'north203Smoke' }
// Internal Demeure: unchanged persistence tests and provenance.
tasks.register('demeureSmoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.demeure.DemeureSmoke'
}
tasks.named('check') { dependsOn('demeureSmoke') }
tasks.named('jar') { from(rootProject.file('mods/demeure/PROVENANCE.md')) { into 'licenses'; rename { 'Demeure-PROVENANCE.md' } } }
// Launch the production classpath independently of the optional laboratory module.
tasks.register('exportStandaloneLaunch') {
dependsOn tasks.named('runServer').get().taskDependencies.getDependencies(tasks.named('runServer').get())
dependsOn tasks.named('runClient').get().taskDependencies.getDependencies(tasks.named('runClient').get())
doLast {
['server':'runServer','client':'runClient'].each { kind, taskName ->
def launch=tasks.named(taskName).get()
def out=rootProject.layout.buildDirectory.file("standalone/${kind}.args").get().asFile
out.parentFile.mkdirs()
def args=launch.allJvmArgs.findAll { !it.toString().startsWith('@') } + ['-classpath',launch.classpath.asPath,launch.mainClass.get()] + launch.args + launch.argumentProviders.collectMany { it.asArguments().toList() }
out.text=args.collect { '"' + it.toString().replace('\\','\\\\').replace('"','\\"') + '"' }.join('\n') + '\n'
}
}
}
+10 -2
View File
@@ -29,7 +29,9 @@ tasks.register('generateOriginResources') {
double angle=Math.atan2(x-7.5,7.5-y)
return Math.floorMod((int)Math.round(angle/(Math.PI/4)),8)
}
def texture={String name,boolean lit,int mask->
// Sample the eight gem cores, not peripheral highlights that overlap angular sectors.
def hues=[[7,3],[11,3],[12,7],[11,11],[7,12],[3,11],[2,7],[3,3]].collect { xy->pixels[xy[1]*16+xy[0]] }
def texture={String name,boolean lit,int mask,int tint=-1->
def result=new BufferedImage(16,16,BufferedImage.TYPE_INT_ARGB)
for(int y=0;y<16;y++)for(int x=0;x<16;x++) {
int p=pixels[y*16+x],a=(p>>>24)&255,r=(p>>>16)&255,g=(p>>>8)&255,b=p&255
@@ -39,6 +41,11 @@ tasks.register('generateOriginResources') {
if(!lit)grey=(int)Math.round(grey*0.28)
r=grey;g=grey;b=grey
}
if(lit&&s>=0&&tint>=0){
int hue=hues[tint],v=Math.max(r,Math.max(g,b))
int hr=(hue>>>16)&255,hg=(hue>>>8)&255,hb=hue&255,m=Math.max(hr,Math.max(hg,hb))
r=(int)Math.round(hr*v/(double)m);g=(int)Math.round(hg*v/(double)m);b=(int)Math.round(hb*v/(double)m)
}
result.setRGB(x,y,(a<<24)|(r<<16)|(g<<8)|b)
}
def f=new File(root,'assets/sanctuary/textures/block/origin/'+name+'.png');f.parentFile.mkdirs();ImageIO.write(result,'png',f)
@@ -71,7 +78,8 @@ tasks.register('generateOriginResources') {
writeJson('assets/sanctuary/items/origin_block.json',[model:[type:'minecraft:model',model:'sanctuary:block/origin/off']])
def anchors=[:]
for(int i=0;i<8;i++) {
writeJson('assets/sanctuary/models/block/origin/anchor_'+i+'.json',model('on_'+(1<<i),true,'minecraft:block/polished_deepslate'))
texture('anchor_'+i,true,255,i)
writeJson('assets/sanctuary/models/block/origin/anchor_'+i+'.json',model('anchor_'+i,true,'minecraft:block/polished_deepslate'))
anchors['lit=false,sector='+i]=[model:'sanctuary:block/origin/anchor_off']
anchors['lit=true,sector='+i]=[model:'sanctuary:block/origin/anchor_'+i]
}
@@ -29,14 +29,15 @@ import net.minecraft.world.phys.*;
/** Actual input, native prediction, play packets and authoritative blocks in a disposable solo world. */
public final class Balance080ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable();
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
Starter039TestSupport.selectFirst(screen);
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Balance080 "+UUID.randomUUID());s.setSeed("80");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Balance080 "+UUID.randomUUID());s.setSeed("80");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
var server=world.getServer();
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;
@@ -45,7 +46,8 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
});
// The base skill places nearby, rejects a normal 4.25-block click, and consumes no stock on rejection.
setup(c,server,0,0,3,Items.COBBLESTONE,false);use(c);server.waitFor(s->player(s).level().getBlockState(pos(3).north()).is(Blocks.COBBLESTONE),100);
setup(c,server,0,0,5,Items.COBBLESTONE,false);use(c);c.waitTicks(12);
setup(c,server,0,0,5,Items.COBBLESTONE,false);
c.runOnClient(m->check(!(m.hitResult instanceof BlockHitResult h&&h.getType()==HitResult.Type.BLOCK&&h.getBlockPos().equals(pos(5))),"Rank-zero construction does not show an unreachable support outline"));use(c);c.waitTicks(12);
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(5).north()).isAir()&&p.getMainHandItem().getCount()==64,"Rank zero rejects far placement without consuming stock");});
// Construction alone reaches 5.25 blocks; Mining stays at its own rank-zero reach.
setup(c,server,0,7,6,Items.COBBLESTONE,false);
@@ -54,15 +56,15 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
server.runOnServer(s->check(player(s).level().getBlockState(pos(6)).is(Blocks.STONE),"Construction does not extend mining"));
use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
setup(c,server,0,7,6,Items.COBBLESTONE,true);use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
// beta.081: the newly earned half-block works through the actual pick and play packet.
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,.25));
c.waitFor(m->Math.abs(m.player.getZ()-.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);use(c);
server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
// Outside six blocks, a forged hit is rejected even within the native packet tolerance.
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,-.25));
// beta.232: the seventh block of reach works through the actual pick and play packet.
setup(c,server,0,7,7,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,.25));
c.waitFor(m->Math.abs(m.player.getZ()-.25)<.01,100);c.getInput().lookAt(pos(7));c.waitTicks(5);use(c);
server.waitFor(s->player(s).level().getBlockState(pos(7).north()).is(Blocks.COBBLESTONE),100);
// Outside seven blocks, a forged hit is rejected even within the native packet tolerance.
setup(c,server,0,7,7,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,-.25));
c.waitFor(m->Math.abs(m.player.getZ()+.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,6),Direction.NORTH,pos(6),false)));c.waitTicks(15);
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(6).north()).isAir()&&p.getMainHandItem().getCount()==64,"Six-block maximum is enforced by the server");});
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,7),Direction.NORTH,pos(7),false)));c.waitTicks(15);
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(7).north()).isAir()&&p.getMainHandItem().getCount()==64,"Seven-block maximum is enforced by the server");});
// A deliberately forged out-of-range native packet cannot place or consume an item.
setup(c,server,7,7,8,Items.COBBLESTONE,false);
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,8),Direction.NORTH,pos(8),false)));c.waitTicks(15);
@@ -105,13 +107,13 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
}
}
private static void curve(ServerPlayer p) {
double[] speed={.75,.85,.95,1,1.075,1.15,1.225,1.30},reach={3,3.5,4,4.5,4.75,5,5.25,5.5},buildReach={3,3.5,4,4.5,4.75,5,5.5,6};
double[] speed={.75,.85,.95,1,1.075,1.15,1.225,1.30},reach={3,3.5,4,4.5,4.75,5,5.25,5.5},buildReach={3,3.5,4,4.5,5,5.5,6,7};
var extra=SanctuaryMod.id("test/balance080");var attribute=p.getAttribute(Attributes.BLOCK_BREAK_SPEED);
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));setRanks(p,3,3);
var state=Blocks.STONE.defaultBlockState();float baseline=state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0));
for(int rank=0;rank<=7;rank++) {setRanks(p,rank,rank);near(attribute.getValue(),speed[rank],"Mining speed rank "+rank);near(state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0))/baseline,speed[rank],"Native block breaking progress rank "+rank);near(BuildReach.range(p),buildReach[rank],"Build reach rank "+rank);near(MiningReach.range(p),reach[rank],"Mining reach rank "+rank);}
attribute.addTransientModifier(new AttributeModifier(extra,.5,AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL));setRanks(p,7,7);near(attribute.getValue(),1.95,"Mining composes with external boost");attribute.removeModifier(extra);
var blockRange=p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);blockRange.addTransientModifier(new AttributeModifier(extra,2,AttributeModifier.Operation.ADD_VALUE));near(BuildReach.range(p),8,"Construction preserves external range bonuses");near(MiningReach.range(p),7.5,"Mining preserves external range bonuses");blockRange.removeModifier(extra);
var blockRange=p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);blockRange.addTransientModifier(new AttributeModifier(extra,2,AttributeModifier.Operation.ADD_VALUE));near(BuildReach.range(p),9,"Construction preserves external range bonuses");near(MiningReach.range(p),7.5,"Mining preserves external range bonuses");blockRange.removeModifier(extra);
near(p.blockInteractionRange(),4.5,"Base interactions remain native");near(p.entityInteractionRange(),3,"Entity interactions remain native");
}
private static void setup(ClientGameTestContext c,TestServerContext server,int mining,int building,int z,Item item,boolean offhand) {
@@ -208,7 +208,7 @@ public final class Community158ClientChecks implements FabricClientGameTest {
check(date.getWidth()==discovery.getWidth(),"Calendar matches navigation width");
check(official.getX()==date.getRight()+10&&official.getRight()==screen.width-8,"Bulletin spans map and community panels");
check(date.getMessage().getString().matches(".*\\d{2}:\\d{2}$"),"Clock included in calendar");
check(inhabitant.getY()-progression.getBottom()>progression.getY()-discovery.getBottom(),"Personal and social navigation groups separated");
check(inhabitant.getY()<pauseButton(screen,"sanctuary.cycle.faction").getY()&&pauseButton(screen,"sanctuary.arena.title").getBottom()<discovery.getY()&&progression.getY()>discovery.getBottom(),"Community group precedes the separate adventure group");
var navigation=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getX()==8&&b.getY()>=50&&b.getY()<pauseButton(screen,"menu.options").getY()).toList();
check(navigation.size()==5,"Five navigation destinations");
check(navigation.stream().allMatch(b->b.getBottom()<pauseButton(screen,"menu.options").getY()),"Navigation clears settings");
@@ -0,0 +1,125 @@
package fr.koka.sanctuary.client;
import com.google.gson.Gson;
import fr.koka.sanctuary.community.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.screens.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native layout, selector returns and publication packets in an isolated file-backed solo world. */
public final class Pause233ClientChecks implements FabricClientGameTest {
private final Path config=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json");
private final Path game=FabricLoader.getInstance().getGameDir();
private final String serverId=UUID.randomUUID().toString();
@Override public void runTest(ClientGameTestContext c) {
byte[] previous=checked(()->Files.exists(config)?Files.readAllBytes(config):null);
var intro=new AtomicBoolean(true);
Path fixture=game.resolve("screenshots/zz-pause233.png");
checked(()->{
Files.createDirectories(config.getParent());Files.writeString(config,new Gson().toJson(new CommunityConfig("file",serverId,"jdbc:mariadb://localhost/sanctuary","root","SANCTUARY_COMMUNITY_DB_PASSWORD")));
Files.createDirectories(fixture.getParent());var image=new java.awt.image.BufferedImage(320,180,java.awt.image.BufferedImage.TYPE_INT_RGB);
for(int y=0;y<180;y++)for(int x=0;x<320;x++)image.setRGB(x,y,x<160?0x567445:0xe5cf86);
javax.imageio.ImageIO.write(image,"png",fixture.toFile());return null;
});
fr.koka.sanctuary.testing.QuickTestWorld.enable();
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
press(screen,"sanctuary.progression.enter");intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Pause233 "+UUID.randomUUID());s.setSeed("233");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().setItem(6,new ItemStack(Items.OAK_LOG,17));p.getInventory().setItem(7,new ItemStack(Items.DIAMOND,3));});
String legacy=UUID.randomUUID().toString();
c.runOnClient(m->check(CommunityClient.request(null,"create",legacy,"","notice","event","Ancienne annonce","Texte historique",0,0,r->check(!r.has("error"),"Legacy-compatible notice seeded through the server")),"Seed request dispatched"));
c.waitFor(m->!CommunityClient.pending(),200);c.waitTicks(24);
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();CommunityClient.open(new PauseScreen(true),"notice","",true);edit(m.gui.screen()).setValue("Du bois pour le pont");body(m.gui.screen()).setValue("Une annonce commune, avec une demande facultative.");composer(m.gui.screen(),"notice");press(m.gui.screen(),"sanctuary.community.task_configure");press(m.gui.screen(),"sanctuary.community.location_current");press(m.gui.screen(),"sanctuary.community.materials");edit(m.gui.screen()).setValue("minecraft:oak_log");});
c.waitTicks(3);
c.runOnClient(m->{widgets(m.gui.screen()).filter(CommunityMaterialsScreen.Slot.class::isInstance).map(CommunityMaterialsScreen.Slot.class::cast).filter(w->w.getMessage().getString().startsWith("minecraft:oak_log")).findFirst().orElseThrow().onPress(enter());widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).toList().getLast().setValue("128");press(m.gui.screen(),"sanctuary.community.save");press(m.gui.screen(),"sanctuary.community.reward.none");press(m.gui.screen(),"sanctuary.community.reward.items");edit(m.gui.screen()).setValue("Un repas au village");press(m.gui.screen(),"sanctuary.community.save");check(body(m.gui.screen()).getValue().startsWith("Une annonce commune"),"Text survives task/material selector returns");composer(m.gui.screen(),"notice");});
shot(c,"notice-configured");
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.publish"));
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),400);
server.runOnServer(s->checked(()->{var entry=store(s).list("notice","",0).entries().stream().filter(e->e.title().equals("Du bois pour le pont")).findFirst().orElseThrow();check(entry.category().equals("info")&&entry.task().materials().equals(List.of(new CommunityTask.Material("minecraft:oak_log",128)))&&entry.task().customReward().equals("Un repas au village"),"Requests and rewards persist without notice category selection");check(s.getPlayerList().getPlayers().getFirst().getInventory().getItem(6).getCount()==17&&s.getPlayerList().getPlayers().getFirst().getInventory().getItem(7).getCount()==3,"Publication never consumes real inventory");return null;}));
c.waitTicks(24);
c.runOnClient(m->CommunityClient.open(new PauseScreen(true),"notice",legacy,false));
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.actions")&&!CommunityClient.pending(),300);
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.actions");press(m.gui.screen(),"sanctuary.community.edit");composer(m.gui.screen(),"notice");body(m.gui.screen()).setValue("Texte historique corrigé");press(m.gui.screen(),"sanctuary.community.save");});
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),400);
server.runOnServer(s->checked(()->{var entry=store(s).thread(legacy,0).entry();check(entry.category().equals("event")&&entry.body().equals("Texte historique corrigé"),"Editing preserves the hidden historical category");return null;}));
c.waitTicks(24);
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),"article","",true);edit(m.gui.screen()).setValue("Gazette233");body(m.gui.screen()).setValue("La photo vient avant le titre et le texte.");composer(m.gui.screen(),"article");check(!button(m.gui.screen(),"publish").active,"Screenshot is still mandatory");press(m.gui.screen(),"sanctuary.community.photo_choose");});
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),500);
c.runOnClient(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow().onPress(enter()));
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.photo_assign");check(body(m.gui.screen()).getValue().equals("La photo vient avant le titre et le texte."),"Text survives screenshot selector return");composer(m.gui.screen(),"article");check(button(m.gui.screen(),"publish").active,"Assigned screenshot enables publication");});
shot(c,"gazette-photo-assigned");c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.publish"));
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityArticle.class::isInstance)&&!CommunityClient.pending(),500);
server.runOnServer(s->checked(()->{var entry=store(s).list("article","",0).entries().getFirst();check(entry.title().equals("Gazette233")&&entry.body().equals("La photo vient avant le titre et le texte.")&&!entry.photo().isEmpty(),"Article text and assigned photo survive publication");return null;}));
byte[] saved=server.computeOnServer(s->checked(()->Files.readAllBytes(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"))));
for(boolean unlocked:new boolean[]{false,true}) {
server.runOnServer(s->atlas(s.getPlayerList().getPlayers().getFirst(),unlocked));c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityCard.class::isInstance),300);
c.runOnClient(m->pause(m.gui.screen(),unlocked));shot(c,"pause-"+(unlocked?"unlocked-":"locked-")+language+"-"+scale);
for(String kind:List.of("notice","article")) {
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),kind,"",true);edit(m.gui.screen()).setValue("Brouillon233");body(m.gui.screen()).setValue("Le brouillon suit les changements de langue et de taille.");composer(m.gui.screen(),kind);});
if(!unlocked){shot(c,"compose-"+kind+"-"+language+"-"+scale);c.runOnClient(m->{widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).filter(a->a.maxScrollAmount()>0).findFirst().ifPresent(a->a.setScrollAmount(a.maxScrollAmount()));});shot(c,"compose-bottom-"+kind+"-"+language+"-"+scale);}
}
}
}
server.runOnServer(s->checked(()->{check(Arrays.equals(saved,Files.readAllBytes(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"))),"Reading/resizing old and new posts never rewrites stored categories");return null;}));
System.out.println("PAUSE233_PASS: FR/EN GUI 2/3/4, identity/faction/battle at top, discovery/progression centered, options below, unified notice composer, descriptive task and photo publication, legacy category preservation");
}finally {intro.set(false);checked(()->{if(previous==null)Files.deleteIfExists(config);else Files.write(config,previous);Files.deleteIfExists(fixture);return null;});}
}
private FileCommunityStore store(net.minecraft.server.MinecraftServer s)throws Exception{return new FileCommunityStore(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),serverId);}
private static void atlas(ServerPlayer p,boolean unlocked){var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(p.level().getServer()).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");}
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
private static AbstractWidget widget(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->key(w).equals("sanctuary.community."+key)).findFirst().orElseThrow();}
private static Button button(Screen s,String key){return (Button)widget(s,key);}
private static void composer(Screen s,String kind){
check(widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).noneMatch(w->key(w).startsWith("sanctuary.community.category.")),"No notice category control or label");
var body=body(s);var extra=button(s,kind.equals("notice")?"task_configure":has(s,"sanctuary.community.photo_change")?"photo_change":"photo_choose");
var publish=button(s,has(s,"sanctuary.community.save")?"save":"publish");
if(kind.equals("notice")){var summary=widget(s,"task_summary");check(extra.getY()>body.getBottom()&&summary.getY()>extra.getBottom()&&publish.getY()>summary.getBottom(),"Text, request configuration, summary and Publish are in order");}
else {check(extra.getBottom()<edit(s).getY()&&publish.getY()>body.getBottom(),"Screenshot selector is before title, then text and Publish");widgets(s).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).forEach(w->check(w.getBottom()<extra.getY(),"Photo preview is first, before selector and title"));}
}
private static void pause(Screen s,boolean unlocked){
check((((PauseAtlasAccess)s).sanctuary$pauseMap()!=null)==unlocked,"Pause map appears only after Atlas purchase");
var keys=Set.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title");
var nav=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->keys.contains(key(b))).toList();check(nav.size()==5,"Five Sanctuary navigation actions");
int top=nav.stream().mapToInt(AbstractWidget::getY).min().orElseThrow(),bottom=nav.stream().mapToInt(AbstractWidget::getBottom).max().orElseThrow();
int options=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("menu.options")).findFirst().orElseThrow().getY();
check(bottom+8<=options,"Navigation stays separated from Options/World Options/Quit");
var community=widget(s,"pause_group_community");var adventure=widget(s,"pause_group_adventure");var game=widget(s,"pause_group_game");
check(community.getY()==50&&community.getBottom()<nav.get(0).getY()&&adventure.getBottom()<nav.get(3).getY()&&game.getBottom()<options,"All three headings precede their buttons");
check(nav.stream().map(CommunityPausePanels::key).toList().equals(List.of("sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","sanctuary.progression.blocks","sanctuary.progression.progress")),"Same visual and keyboard order with or without the map");
check(nav.get(0).getBottom()<nav.get(1).getY()&&nav.get(1).getBottom()<nav.get(2).getY()&&nav.get(2).getBottom()<adventure.getY()&&nav.get(3).getBottom()<nav.get(4).getY()&&nav.get(4).getBottom()<game.getY(),"Three distinct titled groups without overlap");
double center=(nav.get(3).getY()+nav.get(4).getBottom())/2.;check(Math.abs(center-s.height/2.)<=.5,"Only discovery/progression are vertically centered");
if(!unlocked){
for(var b:nav)check(Math.abs(b.getX()+b.getWidth()/2.-s.width/2.)<=.5,"Navigation centered horizontally");
check(widget(s,"article").getX()==8&&widget(s,"notice").getX()>nav.getFirst().getRight(),"Gazette remains left and Notice Board right");
}
else check(nav.getFirst().getX()==8&&widget(s,"article").getX()==widget(s,"notice").getX(),"Unlocked map retains left navigation and community feeds on the right");
}
private void shot(ClientGameTestContext c,String name){var path=c.takeScreenshot("pause233-"+name);checked(()->{var dir=game.resolve("../../../../../build/pause233-screenshots").normalize();Files.createDirectories(dir);Files.copy(path,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);return null;});}
private interface Checked<T>{T run()throws Exception;}
private static <T>T checked(Checked<T> action){try{return action.run();}catch(Exception e){throw new AssertionError(e);}}
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.mining.*;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.realtime.RealtimeService;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.screens.PauseScreen;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
/** Held input and real command/result packets, with a non-operator integrated owner. */
public final class Solo232ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable();
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Solo232 "+UUID.randomUUID());s.setSeed("232");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;check(!fr.koka.sanctuary.operator.OperatorService.allowed(p),"Test owner has no operator commands");});
c.runOnClient(m->{var time=m.player.connection.getCommands().getRoot().getChild("sanctuary").getChild("time");check(time.getChild("timezone")!=null&&time.getChild("vanilla")==null,"Solo owner sees timezone but no operator rule controls");m.player.connection.sendCommand("sanctuary time timezone America/New_York");});
server.waitFor(s->RealtimeService.civilTime(s).getZone().equals(ZoneId.of("America/New_York")),200);
server.runOnServer(s->checked(()->{RealtimeService.reload(s);check(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("America/New_York")),"Timezone survives reload without op");}));
c.runOnClient(m->m.player.connection.sendCommand("sanctuary time timezone auto"));
server.waitFor(s->RealtimeService.civilTime(s).getZone().equals(ZoneId.systemDefault()),200);
for(boolean unlocked:new boolean[]{false,true}) {
server.runOnServer(s->ranks(s.getPlayerList().getPlayers().getFirst(),unlocked));
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
for(String language:List.of("fr_fr","en_us")) for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
c.waitForScreen(PauseScreen.class);c.waitTicks(3);
c.runOnClient(m->{var screen=m.gui.screen();var map=((PauseAtlasAccess)screen).sanctuary$pauseMap();check((map!=null)==unlocked,"Map widget exists only after Atlas purchase");
var buttons=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","menu.options","options.worldOptions.button","menu.returnToMenu","menu.disconnect").contains(CommunityPausePanels.key(b))).toList();
check(buttons.size()==8,"All eight pause menu actions remain present");
var article=heading(screen,"sanctuary.community.article");var notice=heading(screen,"sanctuary.community.notice");
if(!unlocked){check(article.getX()==8&&article.getY()==50&&notice.getY()==50,"Locked map uses full-height Gazette on the left and Notice Board on the right");for(var button:buttons)check(article.getRight()<button.getX()&&notice.getX()>button.getRight(),"Community columns do not overlap centered navigation");}
else check(article.getX()==notice.getX()&&article.getY()<notice.getY(),"Unlocked map retains both community feeds stacked on the right");
for(var button:buttons){check(button.getY()>=50&&button.getBottom()<=screen.height-33,"Button stays in pause body");if(!unlocked)check(Math.abs(button.getX()+button.getWidth()/2.-screen.width/2.)<=.5,"Locked-map navigation is horizontally centered");else check(button.getX()==8,"Unlocked layout retains original left column");}
});
String shot="solo232-pause-"+(unlocked?"unlocked-":"locked-")+language+"-"+scale;
var image=c.takeScreenshot(shot);checked(()->{var dir=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("../../../../../build/solo232-screenshots").normalize();java.nio.file.Files.createDirectories(dir);java.nio.file.Files.copy(image,dir.resolve(shot+".png"),java.nio.file.StandardCopyOption.REPLACE_EXISTING);});
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(2);
}
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setOnGround(true);
for(int x=-3;x<=3;x++)for(int z=-2;z<=8;z++){p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.BEDROCK.defaultBlockState());for(int y=1;y<=5;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());}
for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){p.level().setBlockAndUpdate(new BlockPos(x,y,3),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(new BlockPos(x,y,5),Blocks.COBBLESTONE.defaultBlockState());}
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));p.inventoryMenu.broadcastChanges();
});
c.waitFor(m->m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE)&&m.level.getBlockState(new BlockPos(0,2,3)).is(Blocks.STONE),200);c.waitTicks(6);
c.getInput().lookAt(new BlockPos(0,2,3));c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m->MiningClient.previewSize()==4,200);
c.getInput().holdKey(o->o.keyAttack);server.waitFor(s->MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
c.getInput().lookAt(new BlockPos(0,2,5));
server.waitFor(s->s.overworld().getBlockState(new BlockPos(0,2,5)).isAir(),500);
c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(int x=0;x<=1;x++)for(int y=2;y<=3;y++)for(int z:new int[]{3,5})check(p.level().getBlockState(new BlockPos(x,y,z)).isAir(),"Both successive groups are mined while input stays held");check(p.getMainHandItem().getDamageValue()==8,"Eight breaks cost eight native tool uses");});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();MiningManager.forget(p);for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){p.level().setBlockAndUpdate(new BlockPos(x,y,3),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(new BlockPos(x,y,5),Blocks.COBBLESTONE.defaultBlockState());}var fragile=new ItemStack(Items.DIAMOND_PICKAXE);fragile.setDamageValue(fragile.getMaxDamage()-1);p.setItemInHand(InteractionHand.MAIN_HAND,fragile);p.inventoryMenu.broadcastChanges();});
c.waitFor(m->m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE)&&m.level.getBlockState(new BlockPos(0,2,3)).is(Blocks.STONE),200);c.waitTicks(4);
c.getInput().lookAt(new BlockPos(0,2,3));c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m->MiningClient.previewSize()==4,200);c.getInput().holdKey(o->o.keyAttack);
server.waitFor(s->s.getPlayerList().getPlayers().getFirst().getMainHandItem().isEmpty(),300);c.waitTicks(35);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();int remaining=0;for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){if(p.level().getBlockState(new BlockPos(x,y,3)).is(Blocks.STONE))remaining++;check(p.level().getBlockState(new BlockPos(x,y,5)).is(Blocks.COBBLESTONE),"Broken tool cannot restart on another group");}check(remaining==3&&!MiningManager.active(p),"Tool break interrupts held mining after one paid break");});
c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);
System.out.println("SOLO232_PASS: timezone owner permissions and real commands, persistent override, FR/EN pause map gating and centered buttons, held mining follows next target with native costs");
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);}
new Balance080ClientChecks().runTest(c);
}
private static void ranks(ServerPlayer p,boolean atlas){var r=ProgressRecord.INITIAL;var config=ProgressionService.session(p.level().getServer()).config;for(int i=0;i<7;i++)r=r.buy("mining",r.cost("mining",config));if(atlas)r=r.buy("atlas",r.cost("atlas",config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");}
private static AbstractWidget heading(net.minecraft.client.gui.screens.Screen screen,String key){return screen.children().stream().filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
private interface Checked {void run()throws Exception;}
private static void checked(Checked action){try{action.run();}catch(Exception e){throw new RuntimeException(e);}}
private static void check(boolean ok,String reason){if(!ok)throw new AssertionError(reason);}
}
@@ -0,0 +1,45 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.building.YellowStone;
import java.util.*;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
import net.minecraft.core.*;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.*;
/** Checks the baked geometry and yellow tint for every native slab/stair state. No world is opened. */
public final class YellowRock222ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext context){
context.runOnClient(mc->{
int states=0;
for(var block:List.of(YellowStone.BLOCK,YellowStone.STAIRS,YellowStone.SLAB)){
var vanilla=block==YellowStone.BLOCK?Blocks.STONE:block==YellowStone.STAIRS?Blocks.STONE_STAIRS:Blocks.STONE_SLAB;
for(var state:block.getStateDefinition().getPossibleStates()){
var tints=mc.getBlockColors().getTintSources(state);
check(tints.size()==1&&tints.getFirst().colorInWorld(state,null,BlockPos.ZERO)==YellowStone.TINT,"Yellow rock tint missing");
var nativeState=vanilla.withPropertiesOf(state);
var parts=new ArrayList<BlockStateModelPart>();var nativeParts=new ArrayList<BlockStateModelPart>();
var models=mc.getModelManager().getBlockStateModelSet();
models.get(state).collectParts(RandomSource.create(42),parts);
models.get(nativeState).collectParts(RandomSource.create(42),nativeParts);
int faces=0,nativeFaces=0;
var directions=new ArrayList<>(Arrays.asList(Direction.values()));directions.add(null);
for(var direction:directions){
for(var part:parts)for(var quad:part.getQuads(direction)){
var material=quad.materialInfo();
check(material.tintIndex()==0&&material.sprite().contents().name().toString().equals("minecraft:block/stone"),"Wrong/missing texture or untinted face: "+state);
faces++;
}
for(var part:nativeParts)nativeFaces+=part.getQuads(direction).size();
}
check(faces>0&&faces==nativeFaces,"Incomplete native shape: "+state+" faces="+faces+" expected="+nativeFaces);
states++;
}
}
System.out.println("YELLOW_ROCK222_CLIENT_PASS states="+states+" texture=stone tint=fff5d890");
});
}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -25,6 +25,7 @@ import java.util.List;
public final class SharedIsland196ClientChecks implements FabricClientGameTest {
private static final KeyEvent ENTER=new KeyEvent(InputConstants.KEY_RETURN,0,0);
public void runTest(ClientGameTestContext c) {
require(!net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("sanctuary_test"),"Production world editor must work without Test");
for(String locale:List.of("fr_fr","en_us")) {
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;return m.reloadResourcePacks();});
c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
@@ -41,7 +42,7 @@ public final class SharedIsland196ClientChecks implements FabricClientGameTest {
}
var cycle=create.children().stream().filter(CycleButton.class::isInstance).map(CycleButton.class::cast).filter(b->b.getValue() instanceof WorldCreationUiState.WorldTypeEntry).findFirst().orElseThrow();
int attempts=state.getNormalPresetList().size();while(!isSanctuary(state)&&attempts-->0)cycle.onPress(ENTER);
require(isSanctuary(state),"World type cycle reaches Sanctuary");require(size(create).equals("medium"),"Public preset must use current laboratory Medium");
require(isSanctuary(state),"World type cycle reaches Sanctuary");require(size(create).equals("medium"),"Public preset must use current Sanctuary Medium");
require(state.getSettings().selectedDimensions().get(LevelStem.OVERWORLD).orElseThrow().type().value().height()==1600,"Laboratory vertical extent");
state.setSeed("0");state.setGenerateStructures(false);state.setBonusChest(true);
});
@@ -87,14 +88,15 @@ public final class SharedIsland196ClientChecks implements FabricClientGameTest {
state.setWorldType(new WorldCreationUiState.WorldTypeEntry(flat));require(state.getPresetEditor()!=null,"Vanilla flat editor preserved");create.onClose();
});
}
System.out.println("SHARED196_CLIENT_PASS: public laboratory preset, FR/EN, Small/Medium/Large, Cancel/Escape, registry refresh, disk round trip, vanilla types; no screenshots");
System.out.println("SHARED196_CLIENT_PASS: production Sanctuary preset, FR/EN, Small/Medium/Large, Cancel/Escape, registry refresh, disk round trip, vanilla types; no screenshots");
}
private static boolean isSanctuary(WorldCreationUiState s){return s.getWorldType().preset()!=null&&s.getWorldType().preset().getRegisteredName().equals("sanctuary:sanctuary");}
private static String size(CreateWorldScreen screen){
var generator=screen.getUiState().getSettings().selectedDimensions().overworld();require(generator instanceof NoiseBasedChunkGenerator,"Public entry still uses the old generator");
var noise=(NoiseBasedChunkGenerator)generator;
for(String s:List.of("small","medium","large"))if(noise.generatorSettings().is(Identifier.fromNamespaceAndPath("sanctuary_test","shared_island199_"+s)))return s;
throw new AssertionError("Unrecognized laboratory settings");
var size=fr.koka.sanctuarytest.SharedIsland196.size(noise);
require(size!=null,"Unrecognized Sanctuary settings");
return size.name().toLowerCase(java.util.Locale.ROOT);
}
private static CycleButton<?> sizeButton(Screen s){require(s.getClass().getSimpleName().equals("SharedIsland196Screen"),"Laboratory editor");return s.children().stream().filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();}
private static void choose(Screen screen,String target){var b=sizeButton(screen);int n=3;while(!b.getValue().toString().equals(target)&&n-->0)b.onPress(ENTER);require(b.getValue().toString().equals(target),"Size control reaches "+target);}
@@ -0,0 +1,44 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuarytest.SharedIsland196;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
/** The shipped mod alone must resolve both the accepted terrain and saved historical registry keys. */
public final class Base204GameTests {
@GameTest
public void productionOwnsTerrainWithoutOptionalMods(GameTestHelper helper){
helper.assertFalse(FabricLoader.getInstance().isModLoaded("sanctuary_test"),"Production check must run without the lab module");
helper.assertFalse(FabricLoader.getInstance().isModLoaded("demeure"),"Demeure is internal");
var registries=helper.getLevel().registryAccess();
var presets=registries.lookupOrThrow(Registries.WORLD_PRESET);
var current=presets.getValue(Identifier.fromNamespaceAndPath("sanctuary","sanctuary")).createWorldDimensions().overworld();
helper.assertTrue(SharedIsland196.revision(current)==231&&SharedIsland196.size(current)==SharedIsland196.Size.MEDIUM,"Public preset is the accepted Medium island");
for(var size:SharedIsland196.Size.values()){
var generator=presets.getValue(size.id()).createWorldDimensions().overworld();
helper.assertTrue(SharedIsland196.size(generator)==size&&SharedIsland196.revision(generator)==231,"Creation size resolves without Test");
for(int version=196;version<=230;version++){
if(version==222)continue;
var old=presets.getValue(size.id(version)).createWorldDimensions().overworld();
helper.assertTrue(SharedIsland196.revision(old)==version,"Saved profile identity retained");
}
}
helper.succeed();
}
@GameTest
public void peaksRestoresFullHeightOnlyForNewNorth(GameTestHelper helper){
for(long seed:new long[]{0,42,4736390610738281858L}){
var n=new ExpansionIsland("north_test","sanctuary","north","glacial",1024,0,-1024,seed,"peaks","ready",204);
helper.assertTrue(ExpansionRelief.verticalScale(n)>1,"New Peaks must not use the thin .22 scale");
var old=new ExpansionIsland("north_test","sanctuary","north","glacial",1024,0,-1024,seed,"peaks","ready",203);
helper.assertTrue(ExpansionRelief.verticalScale(old)==.22,"Historical scale is unchanged");
var east=new ExpansionIsland("east_test","sanctuary","east","humid",256,1024,0,seed,"natural","ready",204);
helper.assertTrue(ExpansionRelief.verticalScale(east)==.22,"Other directions keep their generation");
}
helper.succeed();
}
}
@@ -0,0 +1,73 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuarytest.SugarCane225;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
public final class Diagonal225GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
@GameTest public void fourClimatesAndSealedWetlands(GameTestHelper h){
for(long seed:new long[]{0,42,2026,-9137})for(var direction:List.of("northeast","northwest","southeast","southwest")){
var i=island(seed,direction,225);var f=DiagonalTerrain225.field(i);var biomes=new HashMap<String,Integer>();int wet=0,land=0;
for(int x=-510;x<=510;x+=9)for(int z=-510;z<=510;z+=9){
var c=f.column(x,z);if(c.edge()<1)continue;land++;biomes.merge(c.biome(),1,Integer::sum);
h.assertTrue(c.top()-c.bottom()<=68,"Excessive mass below diagonal");
h.assertTrue(f.state(x,c.top(),z).isSolidRender(),"Missing surface support");
if(c.water()){
wet++;h.assertTrue(direction.endsWith("east"),"West must be dry");
for(int y=c.top()+1;y<=DiagonalTerrain225.WATER;y++)for(var d:Direction.Plane.HORIZONTAL){
var block=f.state(x+d.getStepX(),y,z+d.getStepZ());
h.assertTrue(block.is(Blocks.WATER)||block.isSolidRender(),"Open wetland at "+x+","+y+","+z);
}
}
}
h.assertTrue(land>7000,"1024 island too small");
var expected=switch(direction){case "northeast"->List.of("swamp","pale_garden");case "northwest"->List.of("meadow","taiga");case "southeast"->List.of("mangrove_swamp","jungle","bamboo_jungle");default->List.of("desert","savanna");};
for(var b:expected)h.assertTrue(biomes.getOrDefault(b,0)>50,"Missing broad habitat "+direction+"/"+seed+" "+b+" "+biomes);
h.assertTrue(direction.endsWith("east")?wet>100:wet==0,"Wrong water distribution "+direction+"/"+seed);
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender()&&!f.column(relay.getX(),relay.getZ()).water(),"Relay in water/void");
// Recreate in a different access order, including exact chunk boundaries.
var before=f.column(16,-16);double density=f.density(16,180,-16);DiagonalTerrain225.release(i);
var again=DiagonalTerrain225.field(i);again.column(-400,400);
h.assertTrue(before.equals(again.column(16,-16))&&density==again.density(16,180,-16)&&relay.equals(again.relay()),"Order-dependent terrain");
DiagonalTerrain225.release(i);
}h.succeed();
}
@GameTest public void directionalPlacementAndHistoricalIsolation(GameTestHelper h){
for(long seed:new long[]{0,42}){
var root=ExpansionIsland.origin(512,seed,225);var islands=new ArrayList<ExpansionIsland>();islands.add(root);
for(var direction:List.of("northeast","southeast","southwest","northwest")){
var options=ExpansionNearby200.candidates(seed,islands,"new_"+direction,"sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0);
h.assertTrue(!options.isEmpty(),"No candidate for "+direction);var next=options.getFirst();
double target=ExpansionIsland.DIRECTIONS.indexOf(direction)*Math.PI/4-Math.PI/2;
double error=Math.atan2(Math.sin(Math.atan2(next.centerZ(),next.centerX())-target),Math.cos(Math.atan2(next.centerZ(),next.centerX())-target));
h.assertTrue(Math.abs(error)<Math.toRadians(15),"Expansion outside anchor fan");
h.assertTrue(islands.stream().noneMatch(next::conflicts)&&DiagonalTerrain225.active(next),"Collision or wrong profile");islands.add(next.ready());
h.assertFalse(DiagonalTerrain225.active(island(seed,direction,224)),"Historical diagonal changed");
}
var east224=new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",224);
var east225=new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",225);
for(int x=700;x<=1350;x+=29)for(int z=-450;z<=450;z+=29)for(int y=60;y<=448;y+=19)
h.assertTrue(EastVolcano223.field(east224).state(x,y,z)==EastVolcano223.field(east225).state(x,y,z),"Accepted East changed");
h.assertTrue(NorthTerrain208.active(new ExpansionIsland("north","sanctuary","north","glacial",1024,0,-1024,seed,"peaks","ready",225)),"North lost");
h.assertTrue(SouthTerrain213.active(new ExpansionIsland("south","sanctuary","south","arid",1024,0,1024,seed,"canyon","ready",225)),"South lost");
h.assertTrue(WestOcean218.active(new ExpansionIsland("west","sanctuary","west","warm",1024,-1024,0,seed,"ocean","ready",225)),"West lost");
}h.succeed();
}
@GameTest public void sugarCaneNeedsActualWaterAndNaturalSoil(GameTestHelper h){
var level=h.getLevel();var p=h.absolutePos(new BlockPos(2,3,2));var chunk=level.getChunkAt(p);
for(int y=0;y<4;y++)level.setBlock(p.above(y),Blocks.AIR.defaultBlockState(),3);
level.setBlock(p.below(),Blocks.SAND.defaultBlockState(),3);
for(var d:Direction.Plane.HORIZONTAL)level.setBlock(p.below().relative(d),Blocks.STONE.defaultBlockState(),3);
h.assertTrue(SugarCane225.plant(level,chunk,p,3)==0,"Dry cane allowed");
level.setBlock(p.below().east(),Blocks.WATER.defaultBlockState(),3);
h.assertTrue(SugarCane225.plant(level,chunk,p,3)==3,"Wet sand rejected");
for(int y=2;y>=0;y--)level.setBlock(p.above(y),Blocks.AIR.defaultBlockState(),3);
level.setBlock(p.below(),Blocks.STONE.defaultBlockState(),3);
h.assertTrue(SugarCane225.plant(level,chunk,p,3)==0,"Cane on rock allowed");h.succeed();
}
}
@@ -0,0 +1,76 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
public final class Diagonal226GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
private static DiagonalTerrain226.Field field(GameTestHelper h,ExpansionIsland i){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.SMALL.id()).createWorldDimensions().overworld();
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);return DiagonalTerrain226.field(i);
}
@GameTest public void northernConservationAndErodedRim(GameTestHelper h){
for(long seed:new long[]{0,42,2026,-9137})for(String direction:List.of("northeast","northwest")){
var i=island(seed,direction,226);var f=field(h,i);var old=DiagonalTerrain225.field(island(seed,direction,225));
int cut=0,edge=0,kept=0,wet=0,shallow=0,deep=0,raised=0;int maxLift=0;
for(int x=-520;x<=520;x+=8)for(int z=-520;z<=520;z+=8){
var a=old.column(x,z);var b=f.column(x,z);
if(direction.equals("northwest")){
if(a.edge()>=180){kept++;h.assertTrue(a.top()==b.top(),"Central plain changed");for(int y=a.bottom()-2;y<=a.top()+2;y+=7)h.assertTrue(old.state(x,y,z)==f.state(x,y,z),"Central material changed");}
if(a.edge()>0&&a.edge()<80){edge++;if(b.top()<a.top()-4)cut++;}
}else{
if(a.water()){
wet++;int depth=DiagonalTerrain226.WATER-b.top();if(depth<=2)shallow++;if(depth>2)deep++;
h.assertTrue(b.water()&&depth>=1,"Lost basin");
for(int y=b.top()+1;y<=DiagonalTerrain226.WATER;y++)for(var d:Direction.Plane.HORIZONTAL){var s=f.state(x+d.getStepX(),y,z+d.getStepZ());h.assertTrue(s.is(Blocks.WATER)||s.isSolidRender(),"Leaking basin at "+x+","+y+","+z);}
}else if(a.edge()>40){int lift=b.top()-a.top();if(lift>15)raised++;maxLift=Math.max(maxLift,lift);if(a.edge()>180)h.assertTrue(b.top()>=a.top(),"Excavated inner dry land");}
}
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL226_NORTH seed={} direction={} rim={}/{} central={} wet={} shallow={} deep={} raised={} maxLift={}",seed,direction,cut,edge,kept,wet,shallow,deep,raised,maxLift);
if(direction.equals("northwest"))h.assertTrue(kept>1500&&cut>edge*.25,"Rim not sufficiently broken: "+cut+"/"+edge);
else h.assertTrue(wet>100&&shallow>wet*.45&&deep>10&&raised>100&&maxLift>40,"Missing shallows/deep lakes/high ground: "+wet+"/"+shallow+"/"+deep+"/"+raised+"/"+maxLift);
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Relay over void");
DiagonalTerrain226.release(i);
}h.succeed();
}
@GameTest public void southernNativeVolumesAndMangroveCoverage(GameTestHelper h){
for(long seed:new long[]{0,42})for(String direction:List.of("southwest","southeast")){
var i=island(seed,direction,226);var f=field(h,i);var habitats=new TreeMap<String,Integer>();int land=0,voids=0,split=0,low=999,high=-1;
for(int x=-480;x<=480;x+=24)for(int z=-480;z<=480;z+=24){
if(Math.hypot(x,z)>480)continue;var c=f.column(x,z);if(c.top()<0){voids++;continue;}land++;low=Math.min(low,c.top());high=Math.max(high,c.top());habitats.merge(c.biome(),1,Integer::sum);
int intervals=0;boolean last=false;for(int y=32;y<=448;y+=4){boolean solid=f.density(x,y,z)>0;if(solid&&!last)intervals++;last=solid;}if(intervals>=2)split++;
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL226_VOLUME seed={} direction={} land={} empty={} split={} range={}..{} habitats={}",seed,direction,land,voids,split,low,high,habitats);
h.assertTrue(land>400&&high-low>60&&split>20,"Native volume flattened: "+direction+" "+land+" "+split+" "+low+".."+high);
if(direction.equals("southeast"))h.assertTrue(habitats.getOrDefault("sanctuary:diagonal_mangrove226",0)>land*.5&&high>290,"Mangrove/verticality missing");
else h.assertTrue(habitats.getOrDefault("desert",0)>100&&habitats.getOrDefault("savanna",0)>100,"Desert/savanna missing");
var pos=f.relay();h.assertTrue(f.state(pos.getX(),pos.getY()-1,pos.getZ()).isSolidRender(),"Unsupported relay");
var before=f.column(16,-16);double value=f.density(16,180,-16);DiagonalTerrain226.release(i);var again=field(h,i);again.column(127,127);
h.assertTrue(before.equals(again.column(16,-16))&&value==again.density(16,180,-16)&&pos.equals(again.relay()),"Unstable terrain on reload");DiagonalTerrain226.release(i);
}h.succeed();
}
@GameTest public void profilesAndNativeFeaturesAreCompatible(GameTestHelper h){
var registry=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET);
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
var biomes=List.copyOf(registry.getValue(size.id()).createWorldDimensions().overworld().getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
}
for(long seed:new long[]{0,42}){
var accepted=new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",225);
var current=new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",226);
for(int x=700;x<=1350;x+=41)for(int z=-450;z<=450;z+=41)for(int y=60;y<=448;y+=23)
h.assertTrue(EastVolcano223.field(accepted).state(x,y,z)==EastVolcano223.field(current).state(x,y,z),"Accepted East changed");
h.assertTrue(NorthTerrain208.active(new ExpansionIsland("north","sanctuary","north","glacial",1024,0,-1024,seed,"peaks","ready",226)),"North lost");
h.assertTrue(SouthTerrain213.active(new ExpansionIsland("south","sanctuary","south","arid",1024,0,1024,seed,"canyon","ready",226)),"South lost");
h.assertTrue(WestOcean218.active(new ExpansionIsland("west","sanctuary","west","warm",1024,-1024,0,seed,"ocean","ready",226)),"West lost");
}
for(var direction:List.of("northeast","southeast","southwest","northwest"))h.assertFalse(DiagonalTerrain226.active(island(42,direction,225)),"Historical world migrated");
h.succeed();
}
}
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
public final class Diagonal227GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
private static DiagonalTerrain227.Field field(GameTestHelper h,ExpansionIsland i){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.SMALL.id()).createWorldDimensions().overworld();
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);return DiagonalTerrain227.field(i);
}
@GameTest public void continuousWetlandsAndBroaderNorthernFractures(GameTestHelper h){
for(long seed:new long[]{0,42,2026,-9137})for(var direction:List.of("northeast","northwest")){
var i=island(seed,direction,227);var f=field(h,i);var old=DiagonalTerrain226.field(island(seed,direction,226));
int raised=0,cuts=0,wet=0,shore=0,maxStep=0,inner=0;
for(int x=-500;x<=500;x+=8)for(int z=-500;z<=500;z+=8){
var c=f.column(x,z);var before=old.column(x,z);
if(direction.equals("northwest")){
if(c.edge()>340){inner++;h.assertTrue(c.top()==before.top(),"Central plains changed");}
if(c.edge()>180&&c.edge()<300&&c.top()<before.top()-4)cuts++;
}else{
if(c.edge()>180&&c.top()>before.top()+15)raised++;
if(c.water())wet++;
if(c.edge()>275&&c.top()>195&&c.top()<210){
shore++;int step=Math.abs(c.top()-f.top(x+1,z));maxStep=Math.max(maxStep,step);
h.assertTrue(step<=3,"Abrupt interior shoreline at "+x+","+z+" step="+step+" seed="+seed+" column="+c+" next="+f.column(x+1,z)+" old="+before);
}
if(c.edge()>0&&c.edge()<100&&c.top()<before.top()-6)cuts++;
}
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL227_NORTH seed={} direction={} raised={} cuts={} wet={} shore={} maxStep={} inner={}",seed,direction,raised,cuts,wet,shore,maxStep,inner);
h.assertTrue(cuts>40,"Fractures did not extend far enough");
if(direction.equals("northeast"))h.assertTrue(raised>100&&wet>100,"Missing interior relief or wetlands");
else h.assertTrue(inner>700,"Central plain too small");
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Relay without support");
// Recompile and query chunk boundaries in another order, without reusing a mutable sample cache.
var column=f.column(16,16);double density=f.density(16,190,16);DiagonalTerrain227.release(i);var again=field(h,i);again.column(-400,127);
h.assertTrue(column.equals(again.column(16,16))&&density==again.density(16,190,16)&&relay.equals(again.relay()),"Order-dependent terrain");
DiagonalTerrain227.release(i);
}h.succeed();
}
@GameTest public void acceptedSouthernSurfacesAndBroadCaveRegions(GameTestHelper h){
for(long seed:new long[]{0,42})for(var direction:List.of("southwest","southeast")){
var i=island(seed,direction,227);var f=field(h,i);var old=DiagonalTerrain226.field(island(seed,direction,226));
int before=0,after=0,lush=0,drip=0,land=0;
for(int x=-460;x<=460;x+=20)for(int z=-460;z<=460;z+=20){
var c=f.column(x,z);if(c.top()<0)continue;land++;
h.assertTrue(c.top()==old.top(x,z),"Accepted surface moved");
for(int y=c.top()-20;y<=c.top()+5;y+=5)h.assertTrue(f.density(x,y,z)==old.density(x,y,z),"Accepted upper 20 blocks changed");
for(int y=40;y<=448;y+=8){
double a=old.density(x,y,z),b=f.density(x,y,z);if(a>0)before++;if(b>0)after++;
if(direction.equals("southeast"))h.assertTrue(a==b,"Mangrove reef shape changed");
if(a<=0&&y<c.top()-28&&y>c.bottom()){
var biome=f.biome(x,y,z);if(biome.endsWith("lush227"))lush++;if(biome.endsWith("dripstone227"))drip++;
}
}
if(direction.equals("southeast"))h.assertTrue(f.biome(x,c.top(),z).equals(old.biome(x,z)),"Surface mangrove biome replaced by caves");
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL227_SOUTH seed={} direction={} before={} after={} lush={} drip={} land={}",seed,direction,before,after,lush,drip,land);
if(direction.equals("southwest"))h.assertTrue(after<before*.72&&after>before*.15,"Insufficient/excessive thinning");
else h.assertTrue(lush>200&&drip>100,"Missing large cave regions");
DiagonalTerrain227.release(i);
}h.succeed();
}
@GameTest public void threePresetsAndHistoricalIsolation(GameTestHelper h){
var registry=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET);
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
var biomes=List.copyOf(registry.getValue(size.id()).createWorldDimensions().overworld().getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
}
for(String direction:List.of("northeast","northwest","southeast","southwest"))h.assertFalse(DiagonalTerrain227.active(island(42,direction,226)),"Historical profile migrated");
h.succeed();
}
}
@@ -0,0 +1,84 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
public final class Diagonal228GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
private static void bind(GameTestHelper h,ExpansionIsland i){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.SMALL.id()).createWorldDimensions().overworld();
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);
}
@GameTest public void lowWetlandsRestoreLakesAndGradualShores(GameTestHelper h){
for(long seed:new long[]{0,42,-104244511769091555L,-5821233228359091706L}){
var i=island(seed,"northeast",228);var oldIsland=island(seed,"northeast",227);bind(h,i);bind(h,oldIsland);
var f=DiagonalTerrain228.field(i);var old=DiagonalTerrain227.field(oldIsland);var base=DiagonalTerrain225.field(island(seed,"northeast",225));
int wet=0,oldWet=0,inside=0,originalWet=0,retainedWet=0,max=0,shore=0;long lower=0;
for(int x=-480;x<=480;x+=8)for(int z=-480;z<=480;z+=8){
var c=f.column(x,z);var before=old.column(x,z);if(c.water())wet++;if(before.water())oldWet++;
max=Math.max(max,c.top());
if(c.edge()>280){
inside++;lower+=before.top()-c.top();
if(base.column(x,z).water()){originalWet++;if(c.water())retainedWet++;}
if(c.top()>=198&&c.top()<213){shore++;h.assertTrue(Math.abs(c.top()-f.top(x+1,z))<=3,"Abrupt lake shore");}
}
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL228_NE seed={} wet={} oldWet={} retained={}/{} max={} meanLower={} shore={}",seed,wet,oldWet,retainedWet,originalWet,max,lower/(double)inside,shore);
h.assertTrue(max<=250,"Wetland relief too tall: "+max);
h.assertTrue(wet>=oldWet&&wet>400&&retainedWet>=originalWet*.95,"Lakes lost under hills");
h.assertTrue(lower>inside*10&&shore>10,"Relief insufficiently lowered");
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Unsupported relay");
var column=f.column(16,16);double density=f.density(16,210,16);DiagonalTerrain228.release(i);bind(h,i);var again=DiagonalTerrain228.field(i);again.column(-113,91);
h.assertTrue(column.equals(again.column(16,16))&&density==again.density(16,210,16)&&relay.equals(again.relay()),"Order-dependent field");
DiagonalTerrain228.release(i);DiagonalTerrain227.release(oldIsland);
}h.succeed();
}
@GameTest public void angularCutsKeepInnerPlainsAndMangroveHabitats(GameTestHelper h){
for(long seed:new long[]{0,42})for(String direction:List.of("northwest","southeast")){
var i=island(seed,direction,228);var oi=island(seed,direction,227);bind(h,i);bind(h,oi);var f=DiagonalTerrain228.field(i);var old=DiagonalTerrain227.field(oi);
int cuts=0,kept=0,center=0,lush=0,drip=0;
for(int x=-460;x<=460;x+=16)for(int z=-460;z<=460;z+=16){
var c=f.column(x,z);var before=old.column(x,z);
if(direction.equals("northwest")&&c.edge()>=280){center++;h.assertTrue(c.top()==before.top(),"Plain roof changed");}
for(int y=40;y<=448;y+=8){
double a=old.density(x,y,z),b=f.density(x,y,z);
h.assertTrue(a>0||b<=0,"Fracture filled an accepted cavity");
if(a>0){if(b>0)kept++;else cuts++;}
if(direction.equals("northwest")&&c.edge()>=280)h.assertTrue(a==b,"Central plain changed");
if(direction.equals("southeast")&&b<=0&&y<c.top()-28&&y>c.bottom()){
var biome=f.biome(x,y,z);if(biome.endsWith("lush227"))lush++;if(biome.endsWith("dripstone227"))drip++;
}
}
if(direction.equals("southeast")&&c.top()>0)h.assertTrue(f.biome(x,c.top(),z).equals(old.biome(x,before.top(),z)),"Surface habitat changed");
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL228_FRACTURES seed={} direction={} cuts={} kept={} center={} lush={} drip={}",seed,direction,cuts,kept,center,lush,drip);
h.assertTrue(cuts>100&&kept>cuts*2,"Fractures absent or excessive");
if(direction.equals("northwest"))h.assertTrue(center>300,"Missing intact plains");else h.assertTrue(lush>200&&drip>100,"Cave regions lost");
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Unsupported relay");
DiagonalTerrain228.release(i);DiagonalTerrain227.release(oi);
}h.succeed();
}
@GameTest public void acceptedSavannaDensityAndMaterialsAreExact(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"southwest",228);var oi=island(seed,"southwest",227);bind(h,i);bind(h,oi);var f=DiagonalTerrain228.field(i);var old=DiagonalTerrain227.field(oi);
for(int x=-480;x<=480;x+=24)for(int z=-480;z<=480;z+=24){
h.assertTrue(f.top(x,z)==old.top(x,z),"Savanna roof changed");
for(int y=32;y<=448;y+=8)h.assertTrue(f.density(x,y,z)==old.density(x,y,z)&&f.state(x,y,z).equals(old.state(x,y,z)),"Savanna shape/material changed");
}
h.assertTrue(f.relay().equals(old.relay()),"Savanna relay moved");DiagonalTerrain228.release(i);DiagonalTerrain227.release(oi);
}h.succeed();
}
@GameTest public void presetsAndFeatureOrderingRetainHistory(GameTestHelper h){
var registry=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET);
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
var biomes=List.copyOf(registry.getValue(size.id()).createWorldDimensions().overworld().getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
}
for(int rev:new int[]{225,226,227})for(String dir:List.of("northeast","northwest","southeast","southwest"))h.assertFalse(DiagonalTerrain228.active(island(42,dir,rev)),"Historical terrain migrated");
h.succeed();
}
}
@@ -0,0 +1,77 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.ChunkPos;
public final class Diagonal229GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
private static void bind(GameTestHelper h,ExpansionIsland i){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.SMALL.id()).createWorldDimensions().overworld();
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);
}
@GameTest public void wetlandUsesSavannaShapeAndShelteredPaleFloors(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"northeast",229);var old=island(seed,"southwest",228);bind(h,i);bind(h,old);var f=DiagonalTerrain229.field(i);var previous=DiagonalTerrain228.field(old);int floors=0;
for(int x=-448;x<=448;x+=32)for(int z=-448;z<=448;z+=32){
var c=f.column(x,z);h.assertTrue(c.top()==previous.top(x,z),"Wetland roof differs from savanna");
h.assertFalse(c.water(),"Old global water plane retained");
for(int y=32;y<=448;y+=8)h.assertTrue(f.density(x,y,z)==previous.density(x,y,z),"Shape differs from accepted savanna");
if(c.top()>0){h.assertTrue(f.biome(x,c.top(),z).equals(DiagonalTerrain229.SWAMP),"Upper swamp absent");h.assertTrue(f.biome(x,c.top()-16,z).equals(DiagonalTerrain229.PALE),"Lower pale habitat absent");}
floors+=DiagonalVegetation229.paleSites(f,new ChunkPos(x>>4,z>>4)).size();
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL229_PALE seed={} sites={}",seed,floors);
h.assertTrue(floors>5,"No room for sheltered pale trees");
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Unsupported relay");
DiagonalTerrain229.release(i);DiagonalTerrain228.release(old);
}h.succeed();
}
@GameTest public void waterRequiresAClosedNaturalDepression(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"northeast",229);bind(h,i);var f=DiagonalTerrain229.field(i);int water=0;
for(int cx=-28;cx<=28;cx+=2)for(int cz=-28;cz<=28;cz+=2){var cp=new ChunkPos(cx,cz);var pool=f.pool(cp);water+=pool.size();
for(var p:pool){h.assertTrue(p.getY()==f.top(p.getX(),p.getZ())+1&&f.density(p.getX(),p.getY()-1,p.getZ())>0&&f.density(p.getX(),p.getY()-2,p.getZ())>0,"Water without natural floor");
h.assertTrue((p.getX()>>4)==cx&&(p.getZ()>>4)==cz,"Pool crossed validation chunk");
for(var d:Direction.Plane.HORIZONTAL){var q=p.relative(d);h.assertTrue(pool.contains(q)||f.density(q.getX(),q.getY(),q.getZ())>0,"Open water edge");}
}
}
var sample=f.pool(new ChunkPos(-4,3));var pos=f.relay();DiagonalTerrain229.release(i);bind(h,i);var again=DiagonalTerrain229.field(i);
h.assertTrue(sample.equals(again.pool(new ChunkPos(-4,3)))&&pos.equals(again.relay()),"Nondeterministic planning");
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL229_POOLS seed={} cells={}",seed,water);DiagonalTerrain229.release(i);
}h.succeed();
}
@GameTest public void savannaKeepsShapeAndGainsTwoLowerCaveHabitats(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"southwest",229);var oi=island(seed,"southwest",228);bind(h,i);bind(h,oi);var f=DiagonalTerrain229.field(i);var old=DiagonalTerrain228.field(oi);int sulfur=0,drip=0;
for(int x=-432;x<=432;x+=24)for(int z=-432;z<=432;z+=24){var c=f.column(x,z);
for(int y=32;y<=448;y+=8){h.assertTrue(f.density(x,y,z)==old.density(x,y,z),"Savanna shape changed");
if(y>=c.top()-12)h.assertTrue(f.state(x,y,z).equals(old.state(x,y,z)),"Savanna surface changed");
String b=f.biome(x,y,z);h.assertFalse(b.contains("lush"),"Lush caves in dry southwest");if(b.equals(DiagonalTerrain229.SULFUR))sulfur++;if(b.equals(DiagonalTerrain229.DRIP))drip++;
}
}
h.assertTrue(sulfur>100&&drip>100,"Missing cave region");DiagonalTerrain229.release(i);DiagonalTerrain228.release(oi);
}h.succeed();
}
@GameTest public void acceptedMangroveAndPlainsAreExact(GameTestHelper h){
for(long seed:new long[]{0,42})for(String direction:List.of("northwest","southeast")){
var i=island(seed,direction,229);var oi=island(seed,direction,228);bind(h,i);bind(h,oi);var f=DiagonalTerrain229.field(i);var old=DiagonalTerrain228.field(oi);
for(int x=-432;x<=432;x+=32)for(int z=-432;z<=432;z+=32)for(int y=32;y<=448;y+=12)
h.assertTrue(f.density(x,y,z)==old.density(x,y,z)&&f.state(x,y,z).equals(old.state(x,y,z))&&f.biome(x,y,z).equals(old.biome(x,y,z)),"Validated region changed");
h.assertTrue(f.relay().equals(old.relay()),"Relay moved");DiagonalTerrain229.release(i);DiagonalTerrain228.release(oi);
}h.succeed();
}
@GameTest public void presetsAndFeatureOrderingRetainHistory(GameTestHelper h){
var registry=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET);
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
var biomes=List.copyOf(registry.getValue(size.id()).createWorldDimensions().overworld().getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
}
for(int rev:new int[]{225,226,227,228})for(String dir:List.of("northeast","northwest","southeast","southwest"))h.assertFalse(DiagonalTerrain229.active(island(42,dir,rev)),"Historical terrain migrated");
h.succeed();
}
}
@@ -0,0 +1,93 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.ChunkPos;
public final class Diagonal230GameTests {
private static ExpansionIsland island(long seed,String direction,int revision){return new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",revision);}
private static void bind(GameTestHelper h,ExpansionIsland i){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.SMALL.id()).createWorldDimensions().overworld();
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);
}
@GameTest public void wetlandKeepsShapeAndOpensLowerPaleTerraces(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"northeast",230);var old=island(seed,"southwest",228);bind(h,i);bind(h,old);var f=DiagonalTerrain230.field(i);var previous=DiagonalTerrain228.field(old);int floors=0,open=0,sheltered=0;
for(int x=-448;x<=448;x+=32)for(int z=-448;z<=448;z+=32){
var c=f.column(x,z);h.assertTrue(c.top()==previous.top(x,z),"Wetland roof differs from savanna");
h.assertFalse(c.water(),"Old global water plane retained");
for(int y=32;y<=448;y+=8)h.assertTrue(f.density(x,y,z)==previous.density(x,y,z),"Shape differs from accepted savanna");
if(c.top()>0){h.assertTrue(f.biome(x,f.paleLimit(x,z),z).equals(DiagonalTerrain230.SWAMP),"Upper swamp absent");h.assertTrue(f.biome(x,f.paleLimit(x,z)-1,z).equals(DiagonalTerrain230.PALE),"Lower pale habitat absent");}
for(var p:DiagonalVegetation230.paleSites(f,new ChunkPos(x>>4,z>>4))){floors++;h.assertTrue(f.lower(p.getX(),p.getY(),p.getZ()),"Pale tree on upper plateau");if(p.getY()==f.top(p.getX(),p.getZ())+1)open++;else sheltered++;}
}
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL230_PALE seed={} sites={} open={} sheltered={}",seed,floors,open,sheltered);
h.assertTrue(open>20&&sheltered>5,"Missing lower open terraces or sheltered trees");
var relay=f.relay();h.assertTrue(f.state(relay.getX(),relay.getY()-1,relay.getZ()).isSolidRender(),"Unsupported relay");
DiagonalTerrain230.release(i);DiagonalTerrain228.release(old);
}h.succeed();
}
@GameTest public void waterRequiresAClosedNaturalDepression(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"northeast",230);bind(h,i);var f=DiagonalTerrain230.field(i);int water=0;
for(int cx=-28;cx<=28;cx+=2)for(int cz=-28;cz<=28;cz+=2){var cp=new ChunkPos(cx,cz);var pool=f.pool(cp);water+=pool.size();
for(var p:pool){h.assertTrue(p.getY()==f.top(p.getX(),p.getZ())+1&&f.density(p.getX(),p.getY()-1,p.getZ())>0&&f.density(p.getX(),p.getY()-2,p.getZ())>0,"Water without natural floor");
h.assertTrue((p.getX()>>4)==cx&&(p.getZ()>>4)==cz,"Pool crossed validation chunk");
for(var d:Direction.Plane.HORIZONTAL){var q=p.relative(d);h.assertTrue(pool.contains(q)||f.density(q.getX(),q.getY(),q.getZ())>0,"Open water edge");}
}
}
var sample=f.pool(new ChunkPos(-4,3));var pos=f.relay();DiagonalTerrain230.release(i);bind(h,i);var again=DiagonalTerrain230.field(i);
h.assertTrue(sample.equals(again.pool(new ChunkPos(-4,3)))&&pos.equals(again.relay()),"Nondeterministic planning");
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("DIAGONAL230_POOLS seed={} cells={}",seed,water);DiagonalTerrain230.release(i);
}h.succeed();
}
@GameTest public void savannaKeepsShapeAndGainsTwoLowerCaveHabitats(GameTestHelper h){
for(long seed:new long[]{0,42}){
var i=island(seed,"southwest",230);var oi=island(seed,"southwest",229);bind(h,i);bind(h,oi);var f=DiagonalTerrain230.field(i);var old=DiagonalTerrain229.field(oi);int sulfur=0,drip=0;
for(int x=-432;x<=432;x+=24)for(int z=-432;z<=432;z+=24){var c=f.column(x,z);
for(int y=32;y<=448;y+=8){h.assertTrue(f.density(x,y,z)==old.density(x,y,z),"Savanna shape changed");
if(y>=c.top()-12)h.assertTrue(f.state(x,y,z).equals(old.state(x,y,z)),"Savanna surface changed");
String b=f.biome(x,y,z);h.assertFalse(b.contains("lush"),"Lush caves in dry southwest");if(b.equals(DiagonalTerrain230.SULFUR))sulfur++;if(b.equals(DiagonalTerrain230.DRIP))drip++;
}
}
h.assertTrue(sulfur>100&&drip>100,"Missing cave region");DiagonalTerrain230.release(i);DiagonalTerrain229.release(oi);
}h.succeed();
}
@GameTest public void acceptedMangroveAndPlainsAreExact(GameTestHelper h){
for(long seed:new long[]{0,42})for(String direction:List.of("northwest","southeast")){
var i=island(seed,direction,230);var oi=island(seed,direction,228);bind(h,i);bind(h,oi);var f=DiagonalTerrain230.field(i);var old=DiagonalTerrain228.field(oi);
for(int x=-432;x<=432;x+=32)for(int z=-432;z<=432;z+=32)for(int y=32;y<=448;y+=12)
h.assertTrue(f.density(x,y,z)==old.density(x,y,z)&&f.state(x,y,z).equals(old.state(x,y,z))&&f.biome(x,y,z).equals(old.biome(x,y,z)),"Validated region changed");
h.assertTrue(f.relay().equals(old.relay()),"Relay moved");DiagonalTerrain230.release(i);DiagonalTerrain228.release(oi);
}h.succeed();
}
@GameTest public void yellowRockRetainsNativeOreBlocksAndStoneHosts(GameTestHelper h){
var i=island(42,"southwest",230);bind(h,i);var f=DiagonalTerrain230.field(i);var chunk=h.getLevel().getChunk(0,0);var p=new BlockPos.MutableBlockPos();
for(int x=0;x<16;x++)for(int z=0;z<16;z++)for(int y=100;y<240;y++)chunk.setBlockState(p.set(x,y,z),net.minecraft.world.level.block.Blocks.STONE.defaultBlockState(),0);
var ore=new BlockPos(8,170,8);var sulfur=new BlockPos(5,160,5);var diamond=new BlockPos(10,180,10);
chunk.setBlockState(ore,net.minecraft.world.level.block.Blocks.IRON_ORE.defaultBlockState(),0);
chunk.setBlockState(diamond,net.minecraft.world.level.block.Blocks.DIAMOND_ORE.defaultBlockState(),0);
chunk.setBlockState(sulfur,net.minecraft.world.level.block.Blocks.SULFUR.defaultBlockState(),0);
DiagonalTerrain230.finishSavanna(i,chunk);int yellow=0,stone=0;
for(int x=0;x<16;x++)for(int z=0;z<16;z++)for(int y=100;y<240;y++){
var state=chunk.getBlockState(p.set(x,y,z));if(state.is(fr.koka.sanctuary.building.YellowStone.BLOCK))yellow++;if(state.is(net.minecraft.world.level.block.Blocks.STONE))stone++;
}
h.assertTrue(yellow>500&&stone>500,"Missing mixed geology");
h.assertTrue(chunk.getBlockState(ore).is(net.minecraft.world.level.block.Blocks.IRON_ORE)&&chunk.getBlockState(diamond).is(net.minecraft.world.level.block.Blocks.DIAMOND_ORE)&&chunk.getBlockState(sulfur).is(net.minecraft.world.level.block.Blocks.SULFUR),"Ore or sulfur recoloured");
for(var d:Direction.values())h.assertTrue(chunk.getBlockState(ore.relative(d)).is(net.minecraft.world.level.block.Blocks.STONE),"Ore stone host recoloured");
DiagonalTerrain230.release(i);h.succeed();
}
@GameTest public void presetsAndFeatureOrderingRetainHistory(GameTestHelper h){
var registry=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET);
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
var biomes=List.copyOf(registry.getValue(size.id()).createWorldDimensions().overworld().getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
}
for(int rev:new int[]{225,226,227,228,229})for(String dir:List.of("northeast","northwest","southeast","southwest"))h.assertFalse(DiagonalTerrain230.active(island(42,dir,rev)),"Historical terrain migrated");
h.succeed();
}
}
@@ -0,0 +1,50 @@
package fr.koka.sanctuary.gametest;
import java.util.*;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.Blocks;
public final class Diagonal231GameTests {
@GameTest public void newRevisionPreservesAcceptedTerrain(GameTestHelper h){
var preset=h.getLevel().registryAccess().lookupOrThrow(Registries.WORLD_PRESET).getValue(fr.koka.sanctuarytest.SharedIsland196.Size.LARGE.id()).createWorldDimensions().overworld();
for(long seed:new long[]{0,42})for(String direction:List.of("northeast","northwest","southeast","southwest")){
var i=new ExpansionIsland("test","sanctuary",direction,DiagonalTerrain225.climate(direction),1024,0,0,seed,"natural","ready",231);
var old=new ExpansionIsland("test","sanctuary",direction,i.climate(),1024,0,0,seed,"natural","ready",230);
var adapter=new ExpansionChunkGenerator(new ExpansionBiomeSource(List.copyOf(preset.getBiomeSource().possibleBiomes())),512);
adapter.initialize(h.getLevel(),List.of(i));adapter.randomState(i);
var f=DiagonalTerrain231.field(i);var accepted=DiagonalTerrain230.field(old);
h.assertTrue(f==accepted,"Terrain must reuse exactly the accepted field");
h.assertFalse(DiagonalTerrain231.active(old),"Old worlds migrated");
for(int x=-432;x<=432;x+=48)for(int z=-432;z<=432;z+=48)for(int y=48;y<448;y+=24)
h.assertTrue(f.state(x,y,z).equals(accepted.state(x,y,z)),"Material changed");
DiagonalTerrain231.release(i);
}
var biomes=List.copyOf(preset.getBiomeSource().possibleBiomes());
net.minecraft.world.level.biome.FeatureSorter.buildFeaturesPerStep(biomes,b->b.value().getGenerationSettings().features(),true);
h.succeed();
}
@GameTest public void plantsSurviveOnPaleSoilAndDoNotOverwriteFeatures(GameTestHelper h){
var level=h.getLevel();var p=h.absolutePos(new BlockPos(2,3,2));
for(var plant:List.of(Blocks.SHORT_GRASS,Blocks.TALL_GRASS,Blocks.CLOSED_EYEBLOSSOM,Blocks.PALE_MOSS_CARPET)){
level.setBlock(p.below(),Blocks.PALE_MOSS_BLOCK.defaultBlockState(),2);
level.setBlock(p,Blocks.AIR.defaultBlockState(),2);level.setBlock(p.above(),Blocks.AIR.defaultBlockState(),2);
h.assertTrue(DiagonalVegetation231.placePlant(level,p,plant.defaultBlockState()),"Native plant cannot root on pale moss: "+plant);
h.assertTrue(level.getBlockState(p).canSurvive(level,p),"Unsupported plant");
if(plant==Blocks.TALL_GRASS)h.assertTrue(level.getBlockState(p.above()).is(Blocks.TALL_GRASS),"Missing upper half");
}
for(var obstacle:List.of(Blocks.WATER,Blocks.POINTED_DRIPSTONE,Blocks.PALE_OAK_LOG)){
level.setBlock(p,obstacle.defaultBlockState(),2);
h.assertFalse(DiagonalVegetation231.placePlant(level,p,Blocks.SHORT_GRASS.defaultBlockState()),"Plant replaced existing feature");
h.assertTrue(level.getBlockState(p).is(obstacle),"Obstacle changed");
}
level.setBlock(p,Blocks.AIR.defaultBlockState(),2);level.setBlock(p.above(),Blocks.STONE.defaultBlockState(),2);
h.assertFalse(DiagonalVegetation231.placePlant(level,p,Blocks.TALL_GRASS.defaultBlockState()),"Tall grass through ceiling");
level.setBlock(p.below(),Blocks.PALE_OAK_LEAVES.defaultBlockState(),2);
h.assertFalse(DiagonalVegetation231.placePlant(level,p,Blocks.SHORT_GRASS.defaultBlockState()),"Grass on canopy");
h.succeed();
}
}
@@ -0,0 +1,59 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
public final class East223GameTests {
@GameTest public void closedCraterFloatingConeAndJungleHabitats(GameTestHelper h){
for(long seed:new long[]{0,42,2026,-6763683964522674921L}){
var i=east(seed);var f=EastVolcano223.field(i);int lava=0,voids=0;var habitats=new HashMap<String,Integer>();
double summit=0,flank=0;
for(int n=0;n<32;n++){
double a=n*Math.PI/16;int x=i.centerX()+(int)(Math.cos(a)*94),z=(int)(Math.sin(a)*94);summit+=f.top(x,z);
x=i.centerX()+(int)(Math.cos(a)*365);z=(int)(Math.sin(a)*365);flank+=f.top(x,z);
}
h.assertTrue(summit/32>330&&summit/32-flank/32>85,"Volcano must have a raised rim and lower flanks");
for(int x=i.centerX()-110;x<=i.centerX()+110;x++)for(int z=-110;z<=110;z++)if(f.lava(x,z)){
lava++;h.assertTrue(f.state(x,EastVolcano223.LAVA,z).is(Blocks.LAVA),"Lava missing");
int floor=f.column(x,z).top();h.assertTrue(f.state(x,floor,z).isSolidRender(),"Lava floor pierced");
for(int y=floor+1;y<=EastVolcano223.LAVA;y++)for(var dir:Direction.Plane.HORIZONTAL){
var s=f.state(x+dir.getStepX(),y,z+dir.getStepZ());h.assertTrue(s.is(Blocks.LAVA)||s.isSolidRender(),"Lava wall leaks into air");
}
}
h.assertTrue(lava>3500&&lava<16000,"Missing or oversized crater: "+lava);
for(int x=i.centerX()-490;x<i.centerX()+490;x+=8)for(int z=-490;z<490;z+=8){
if(f.top(x,z)<0)continue;habitats.merge(f.biome(x,z),1,Integer::sum);
h.assertTrue(f.state(x,31,z).isAir(),"Island must float");
for(int y=100;y<f.top(x,z)-20;y+=12)if(f.density(x,y,z)<0&&f.density(x,y+12,z)>0)voids++;
}
for(String biome:List.of("east_giant_jungle223","east_bamboo223","east_sparse_jungle223"))h.assertTrue(habitats.getOrDefault(biome,0)>800,"Habitat missing: "+biome+habitats);
h.assertTrue(voids>100,"Suspended rock lacks caves/undercuts");
var temple=EastTemple223.plan(i);var relay=RegionalRelay215.plan(i);
h.assertTrue(temple.origin().distSqr(relay.anchor())>10000,"Landmarks overlap");
h.assertTrue(relay.anchor().getX()>i.centerX()+300,"Relay must point outwards on East flank");
h.assertTrue(f.state(f.arrival().getX(),f.arrival().getY()-1,f.arrival().getZ()).isSolidRender(),"Arrival unsupported");
EastVolcano223.release(i);RegionalRelay215.release(i);
h.assertTrue(EastVolcano223.field(i).temple().equals(temple.origin()),"Restart changed temple position");
}h.succeed();
}
@GameTest public void currentEastOffersAndAcceptedWestRemainStable(GameTestHelper h){
for(long seed:new long[]{0,42}){
var origin=ExpansionIsland.origin(512,seed,223);
var choices=ExpansionNearby200.candidates(seed,List.of(origin),"east_test","sanctuary","east","tropical",1024,210,0);
h.assertTrue(!choices.isEmpty()&&choices.getFirst().relief().equals("volcano")&&choices.getFirst().centerX()>0,"East must request a volcano ahead of its anchor");
var old=new ExpansionIsland("west","sanctuary","west","warm",1024,-1024,0,seed,"ocean","ready",221);
var next=new ExpansionIsland("west","sanctuary","west","warm",1024,-1024,0,seed,"ocean","ready",223);
var a=WestOcean218.field(old);var b=WestOcean218.field(next);
for(int x=-1520;x<-528;x+=17)for(int z=-496;z<496;z+=17){
h.assertTrue(a.top(x,z)==b.top(x,z)&&a.biome(x,z).equals(b.biome(x,z)),"Accepted West geography changed");
for(int y=70;y<350;y+=11)h.assertTrue(a.state(x,y,z)==b.state(x,y,z),"Accepted West materials changed");
}
h.assertTrue(WestFeatures218.plan(old).equals(WestFeatures218.plan(next)),"Accepted West details changed");
}h.succeed();
}
private static ExpansionIsland east(long seed){return new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",223);}
}
@@ -0,0 +1,56 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.placement.BlockPredicateFilter;
public final class East224GameTests {
@GameTest public void acceptedGeometryAndColdRegionsStayIdentical(GameTestHelper h){
for(long seed:new long[]{0,42,2026,-6763683964522674921L}){
var old=island(seed,223);var next=island(seed,224);
var a=EastVolcano223.field(old);var b=EastVolcano223.field(next);
for(int x=530;x<1519;x+=17)for(int z=-494;z<495;z+=17){
h.assertTrue(a.top(x,z)==b.top(x,z)&&a.lava(x,z)==b.lava(x,z),"Accepted surface or crater changed");
for(int y=20;y<449;y+=13){
h.assertTrue(a.density(x,y,z)==b.density(x,y,z),"Accepted ravines/caves changed");
var ca=a.column(x,z);var sa=a.state(x,y,z);var sb=b.state(x,y,z);
h.assertTrue(sa.isAir()==sb.isAir()&&sa.is(Blocks.LAVA)==sb.is(Blocks.LAVA),"Material treatment changed terrain occupancy");
if((!ca.hot()&&ca.radius()>=115)||y<ca.top()-7)h.assertTrue(sa==sb,"Materials changed beyond the crater skin");
}
}
h.assertTrue(a.arrival().equals(b.arrival())&&a.temple().equals(b.temple())&&a.relay().equals(b.relay()),"Landmarks moved");
int oldObsidian=0,obsidian=0,magma=0,dryMagma=0;
for(int x=884;x<=1164;x++)for(int z=-140;z<=140;z++){
int y=b.top(x,z);if(y<0)continue;
var before=a.state(x,y,z);var after=b.state(x,y,z);
if(before.is(Blocks.OBSIDIAN))oldObsidian++;
if(after.is(Blocks.OBSIDIAN))obsidian++;
if(after.is(Blocks.MAGMA_BLOCK)){magma++;if(y>=EastVolcano223.LAVA)dryMagma++;}
}
h.assertTrue(obsidian>oldObsidian*1.3,"Obsidian shore did not expand: "+oldObsidian+" -> "+obsidian);
h.assertTrue(magma>40&&dryMagma>15,"Hot shore fissures missing "+magma+"/"+dryMagma);
}h.succeed();
}
@GameTest public void giantTreeRejectsCanopyAndAcceptsNaturalSoil(GameTestHelper h){
var feature=h.getLevel().registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.getValue(Identifier.fromNamespaceAndPath("sanctuary","east_giant_trees224"));
var filters=feature.placement().stream().filter(BlockPredicateFilter.class::isInstance).map(BlockPredicateFilter.class::cast).toList();
h.assertTrue(!filters.isEmpty(),"Giant tree has no ground admission check");
var pos=h.absolutePos(new net.minecraft.core.BlockPos(1,3,1));
h.setBlock(1,3,1,Blocks.AIR);
for(var support:new net.minecraft.world.level.block.Block[]{Blocks.JUNGLE_LEAVES,Blocks.JUNGLE_LOG,Blocks.AIR,Blocks.STONE}){
h.setBlock(1,2,1,support);
h.assertFalse(filters.stream().allMatch(f->f.predicate().test(h.getLevel(),pos)),"Tree admitted above "+support);
}
for(var support:new net.minecraft.world.level.block.Block[]{Blocks.GRASS_BLOCK,Blocks.DIRT,Blocks.PODZOL}){
h.setBlock(1,2,1,support);
h.assertTrue(filters.stream().allMatch(f->f.predicate().test(h.getLevel(),pos)),"Natural soil rejected "+support);
}
h.succeed();
}
private static ExpansionIsland island(long seed,int revision){return new ExpansionIsland("east","sanctuary","east","tropical",1024,1024,0,seed,"volcano","ready",revision);}
}
@@ -0,0 +1,47 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Regressions: chunk-aligned ecological stair steps and deformation of the raw field. */
public final class North205GameTests {
@GameTest
public void reliefMeasurementsCrossChunkEdgesContinuously(GameTestHelper h) {
var island=new ExpansionIsland("north_continuity","sanctuary","north","glacial",1024,0,-1024,42,"peaks","ready",205);
NorthTerrain205.bind(island,new DensitySampler(){
public float sampleValue(SamplerContext c,int x,int y,int z){return (float)Math.min(y-40.01,201.01+x*.25+(z+1024)*.5-y);}
public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
});
try {
var f=NorthTerrain205.field(island);
// A plane must remain a plane at every block, including negative coordinates and 8/16 seams.
for(int x=-33;x<=33;x++)for(int z=-1057;z<=-991;z++) {
double expected=201+x*.25+(z+1024)*.5;
h.assertTrue(Math.abs(f.column(x,z).top()-expected)<1e-9,"Quantized height at "+x+","+z);
h.assertTrue(Math.abs(f.column(x+1,z).top()-f.column(x,z).top()-.25)<1e-9,"Chunk boundary jump");
}
var points=java.util.stream.IntStream.range(0,128).mapToObj(n->new int[]{n%33-16,-1040+n/4}).toList();
var before=points.stream().map(p->f.cover(p[0],p[1])).toList();
var after=points.parallelStream().map(p->f.cover(p[0],p[1])).toList();
h.assertTrue(before.equals(after),"Ecology changes with worker or request order");
} finally {NorthTerrain205.release(island);}
h.succeed();
}
@GameTest
public void northNoiseHasNoPeaksWarp(GameTestHelper h) {
var n=new ExpansionIsland("north_native","sanctuary","north","glacial",1024,0,-1024,42,"peaks","ready",205);
h.assertTrue(ExpansionRelief.horizontalScale(n)==1&&ExpansionRelief.verticalScale(n)==1,"Noise wavelengths must not be stretched");
for(int x=-256;x<=256;x+=16)for(int z=-1280;z<=-768;z+=16){
var c=ExpansionRelief.column(n,x,z);
h.assertTrue(c.uplift()==0&&c.canyonWeight()==0&&c.craterWeight()==0,"Added mountain layer remains");
for(int y=80;y<=320;y+=24)h.assertTrue(ExpansionRelief.sourceY(n,c,y)==y,"Y is remapped");
}
var old=new ExpansionIsland("old","sanctuary","north","glacial",1024,0,-1024,42,"peaks","ready",204);
h.assertTrue(ExpansionRelief.verticalScale(old)>1,"Historical profile changed");
var east=new ExpansionIsland("east","sanctuary","east","tropical",256,1024,0,42,"natural","ready",205);
h.assertTrue(ExpansionRelief.verticalScale(east)==.22,"Other expansions changed");
h.succeed();
}
}
@@ -0,0 +1,35 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class North206GameTests {
@GameTest
public void subtractiveShelfPreservesRoofAndVariesThickness(GameTestHelper h){
var island=new ExpansionIsland("north_shelf","sanctuary","north","glacial",1024,0,-1024,42,"peaks","ready",206);
var raw=new DensitySampler(){
public float sampleValue(SamplerContext c,int x,int y,int z){return (float)Math.min(y-30.1,240.1-y);}
public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
var shelf=new NorthShelf206(island,raw);var context=SamplerContext.builder().build();
double low=999,high=0;int cavities=0;
for(int x=-192;x<=192;x+=8)for(int z=-1216;z<=-832;z+=8){
var c=shelf.column(context,x,z);double thickness=c.top()-c.bottom();low=Math.min(low,thickness);high=Math.max(high,thickness);
for(int y=1;y<=280;y++){
float before=raw.sampleValue(context,x,y,z),after=shelf.sampleValue(context,x,y,z);
h.assertTrue(after<=before,"Shelf adds terrain");
if(y>=220)h.assertTrue(before==after,"Surface shell changed");
if(y<c.bottom()-4)h.assertTrue(after<0,"Underside retained");
if(y<216&&after<0&&before>0&&y>c.bottom()+16)cavities++;
}
}
h.assertTrue(low>=40&&high<=99&&high-low>20,"Thickness not variable: "+low+".."+high);
h.assertTrue(cavities>100,"No open faults");
var expected=java.util.stream.IntStream.range(0,256).mapToObj(n->shelf.sampleValue(context,n-128,190,-1024)).toList();
var parallel=java.util.stream.IntStream.range(0,256).parallel().mapToObj(n->shelf.sampleValue(SamplerContext.builder().build(),n-128,190,-1024)).toList();
h.assertTrue(expected.equals(parallel),"Worker order changes terrain");
h.succeed();
}
}
@@ -0,0 +1,43 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.properties.*;
import net.minecraft.core.Direction;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class North207GameTests {
@GameTest
public void crownsAreLocalAndPreserveAcceptedCavities(GameTestHelper h){
var i=new ExpansionIsland("north","sanctuary","north","glacial",1024,0,-1024,42,"peaks","ready",207);
var raw=new DensitySampler(){
public float sampleValue(SamplerContext c,int x,int y,int z){return (float)Math.min(y-30.1,240.1-y);}
public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
var shelf=new NorthShelf206(i,raw);var relief=new NorthRelief207(shelf);var context=SamplerContext.builder().build();
int raised=0,unchanged=0;double highest=0;
for(int x=-320;x<=320;x+=8)for(int z=-1344;z<=-704;z+=8){
double uplift=relief.uplift(x,z);if(uplift>1)raised++;else unchanged++;highest=Math.max(highest,uplift);
for(int y=100;y<=230;y+=10)h.assertTrue(relief.sampleValue(context,x,y,z)==shelf.sampleValue(context,x,y,z),"Accepted underground changed");
h.assertTrue(relief.sampleValue(context,x,310,z)<0,"Crown exceeds ceiling");
}
h.assertTrue(raised>100&&unchanged>raised*2&&highest>30&&highest<60,"Relief must be local and visible: "+raised+"/"+unchanged+"/"+highest);
var f=new NorthTerrain207.Field(i,relief);int snowy=0,dry=0;
for(int x=-300;x<=300;x+=8)for(int z=-1324;z<=-724;z+=8){if(f.forestSnow(x,z))snowy++;else dry++;}
h.assertTrue(snowy>500&&dry>500,"Snow erases one climate");
var serial=java.util.stream.IntStream.range(0,200).mapToObj(n->relief.sampleValue(context,n-100,255,-1100)).toList();
var parallel=java.util.stream.IntStream.range(0,200).parallel().mapToObj(n->relief.sampleValue(SamplerContext.builder().build(),n-100,255,-1100)).toList();
h.assertTrue(serial.equals(parallel),"Worker order changes crowns");h.succeed();
}
@GameTest
public void mansionSpruceKeepsStairsAndDoorsFunctional(GameTestHelper h){
var stairs=Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,Direction.WEST).setValue(StairBlock.HALF,Half.TOP).setValue(StairBlock.SHAPE,StairsShape.INNER_LEFT);
var changed=NorthMansion207.palette(stairs);
h.assertTrue(changed.is(Blocks.SPRUCE_STAIRS)&&changed.getValue(StairBlock.FACING)==Direction.WEST&&changed.getValue(StairBlock.HALF)==Half.TOP&&changed.getValue(StairBlock.SHAPE)==StairsShape.INNER_LEFT,"Stair orientation lost");
var door=Blocks.DARK_OAK_DOOR.defaultBlockState().setValue(DoorBlock.HALF,DoubleBlockHalf.UPPER).setValue(DoorBlock.HINGE,DoorHingeSide.RIGHT).setValue(DoorBlock.OPEN,true);
changed=NorthMansion207.palette(door);
h.assertTrue(changed.is(Blocks.SPRUCE_DOOR)&&changed.getValue(DoorBlock.HALF)==DoubleBlockHalf.UPPER&&changed.getValue(DoorBlock.HINGE)==DoorHingeSide.RIGHT&&changed.getValue(DoorBlock.OPEN),"Door properties lost");h.succeed();
}
}
@@ -0,0 +1,80 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.expansion.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class North208GameTests {
private static NorthRelief208 terrain(long seed){
var i=new ExpansionIsland("north","sanctuary","north","glacial",1024,0,-1024,seed,"peaks","ready",208);
var raw=new DensitySampler(){
public float sampleValue(SamplerContext c,int x,int y,int z){return (float)Math.min(y-30.1,240.1-y);}
public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
return new NorthRelief208(new NorthShelf206(i,raw));
}
@GameTest
public void pointedLocalRidgesKeepDeepCavities(GameTestHelper h){
for(long seed:new long[]{42,0}){
var r=terrain(seed);var context=SamplerContext.builder().build();
int raised=0,unchanged=0;double highest=0;
for(int x=-320;x<=320;x+=8)for(int z=-1344;z<=-704;z+=8){
double uplift=r.uplift(x,z);if(uplift>1)raised++;else unchanged++;highest=Math.max(highest,uplift);
for(int y=100;y<=230;y+=10)h.assertTrue(r.sampleValue(context,x,y,z)==r.shelf.sampleValue(context,x,y,z),"Accepted underground changed");
h.assertTrue(r.sampleValue(context,x,320,z)<0,"Relief exceeds ceiling");
}
h.assertTrue(raised>100&&unchanged>raised*2&&highest>30&&highest<70,"Relief must be visible and localized: "+raised+"/"+unchanged+"/"+highest);
// The summit must lose several blocks in its first eight horizontal blocks; a smooth dome fails this.
int pointed=0;
for(var c:r.crowns){
int px=c.x(),pz=c.z();double top=-1;
for(int x=c.x()-5;x<=c.x()+5;x++)for(int z=c.z()-5;z<=c.z()+5;z++)if(r.top(x,z)>top){top=r.top(x,z);px=x;pz=z;}
double fall=0;
for(int n=0;n<8;n++){double a=n*Math.PI/4;fall+=top-r.top(px+(int)Math.round(8*Math.cos(a)),pz+(int)Math.round(8*Math.sin(a)));}
if(fall/8>3)pointed++;
}
h.assertTrue(pointed>=3,"Summits still rounded: "+pointed);
var serial=java.util.stream.IntStream.range(0,200).mapToObj(n->r.sampleValue(context,n-100,255,-1100)).toList();
var parallel=java.util.stream.IntStream.range(0,200).parallel().mapToObj(n->r.sampleValue(SamplerContext.builder().build(),n-100,255,-1100)).toList();
h.assertTrue(serial.equals(parallel),"Generation depends on worker order");
}
h.succeed();
}
@GameTest
public void glacialBenchesAreLocalSupportedAndAtDifferentHeights(GameTestHelper h){
for(long seed:new long[]{42,0}){
var r=terrain(seed);var context=SamplerContext.builder().build();
h.assertTrue(r.terraces.size()>=2,"Missing local benches");
h.assertTrue(r.terraces.stream().map(NorthRelief208.Terrace::level).distinct().count()>=2,"Benches all have one height");
for(var t:r.terraces){
h.assertTrue(r.pocket(t.x(),t.z())!=null,"Missing glacial hollow");
for(int x=t.x()-3;x<=t.x()+3;x++)for(int z=t.z()-3;z<=t.z()+3;z++){
h.assertTrue(r.sampleValue(context,x,t.level()-4,z)>0,"Glacial floor hangs over air");
if(r.pocket(x,z)!=null)h.assertTrue(Math.abs(r.top(x,z)-(t.level()-1))<.01,"Inner pocket is not level");
}
}
h.assertTrue(!r.spires.isEmpty(),"Missing bank ice formations");
}
h.succeed();
}
@GameTest
public void glacialBenchCannotBridgeADeepRoofHole(GameTestHelper h){
var reference=terrain(42);var t=reference.terraces.getFirst();int hx=t.x()+4,hz=t.z();
var raw=new DensitySampler(){
public float sampleValue(SamplerContext c,int x,int y,int z){
double top=Math.hypot(x-hx,z-hz)<5?170.1:240.1;
return (float)Math.min(y-30.1,top-y);
}
public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
var r=new NorthRelief208(new NorthShelf206(reference.shelf.island,raw));
var context=SamplerContext.builder().build();
for(int x=hx-3;x<=hx+3;x++)for(int z=hz-3;z<=hz+3;z++){
h.assertTrue(r.uplift(x,z)==0,"A glacial bench bridges a deep hole");
for(int y=218;y<310;y+=4)h.assertTrue(r.sampleValue(context,x,y,z)<0,"Raised pillar over low support");
}
h.succeed();
}
}
@@ -46,6 +46,20 @@ public final class Realtime037GameTests {
h.assertTrue(RealtimeService.setMode(s,false),"Return to native clock");var nativeState=clocks.getInstance(over).packState();h.assertTrue(nativeState.rate()==2&&nativeState.paused()&&nativeState.totalTicks()==first.totalTicks(),"Native parameters restored at current time");
clocks.getInstance(over).loadFrom(new ClockState(nativeState.totalTicks(),0,3,false));RealtimeService.setMode(s,false);RealtimeService.reload(s);
h.assertTrue(clocks.getInstance(over).rate()==3&&!clocks.getInstance(over).isPaused(),"Repeated vanilla command/reload preserves subsequent operator edits");
var contract=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-realtime.json");
var vanillaBefore=clocks.getInstance(over).packState();
h.assertTrue(!node.getChild("timezone").canUse(p.createCommandSourceStack()),"Dedicated non-operator cannot change the world's timezone");
h.assertTrue(commands.execute("sanctuary time timezone Asia/Tokyo",source)==1,"Timezone command accepts IANA names including slash");
h.assertTrue(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo"))&&clocks.getInstance(over).packState().equals(vanillaBefore),"Timezone choice preserves Vanilla mode and live clock");
byte[] chosen=Files.readAllBytes(contract);
h.assertTrue(commands.execute("sanctuary time timezone Asia/Tokyo",source)==1&&java.util.Arrays.equals(chosen,Files.readAllBytes(contract)),"Repeated timezone choice is idempotent");
h.assertTrue(commands.execute("sanctuary time timezone Unknown/Nowhere",source)==0&&java.util.Arrays.equals(chosen,Files.readAllBytes(contract)),"Unknown timezone preserves saved contract");
RealtimeService.reload(s);
h.assertTrue(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo")),"World override survives reload");
Files.writeString(contract,"{external-conflict");
h.assertTrue(commands.execute("sanctuary time timezone Europe/London",source)==0&&RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo"))&&Files.readString(contract).equals("{external-conflict"),"Atomic conflict keeps current timezone and external data");
Files.write(contract,chosen);
h.assertTrue(commands.execute("sanctuary time timezone auto",source)==1&&RealtimeService.civilTime(s).getZone().equals(new RealtimeConfig(true,"Europe/Paris","auto").zone(s.isSingleplayer())),"Auto restores configured timezone");
commands.execute("time set 7000",source);h.assertTrue(clocks.getInstance(over).totalTicks()==7000,"Native command works again after opt-out");
RealtimeService.setMode(s,true);Files.writeString(configPath,new RealtimeConfig(false,"Europe/Paris","auto").json());RealtimeService.reload(s);
h.assertTrue(!RealtimeService.active(h.getLevel())&&clocks.getInstance(over).rate()==3,"Global disable restores the last native rate");

Some files were not shown because too many files have changed in this diff Show More