Compare commits

..
Author SHA1 Message Date
koka 309d0ba6f3 Add permanent Smithing aptitude for anvils and smithing tables in beta.238
Build Sanctuary / build (push) Waiting to run
2026-10-06 16:12:32 +02:00
koka 5dee98106e Keep purchased aptitudes permanent and integrate Pause in beta.237
Build Sanctuary / build (push) Waiting to run
2026-10-06 14:43:01 +02:00
koka d6f3258442 Merge verified beta.235 Pause layout into progression aptitudes 2026-10-06 14:35:34 +02:00
koka 1c8e6437d5 Group aptitudes and unlock enchanting and alchemy in beta.236 2026-10-06 14:25:15 +02:00
koka 00d175cb98 Deliver beta.235 as a verified reversible Pause layout experiment
Build Sanctuary / build (push) Waiting to run
2026-10-06 14:21:31 +02:00
koka a7bdfe6998 Try parallel Pause groups before Atlas and a narrow stack beside the map 2026-10-06 14:16:40 +02:00
koka f9c2ce8835 Deliver beta.234 with verified Story catalogue and local pack
Build Sanctuary / build (push) Waiting to run
2026-10-06 04:50:30 +02:00
koka 3d56d7de7f Verify Story menus under Vulkan and document story authoring 2026-10-06 04:49:56 +02:00
koka f2ac23b4f4 Add Story catalogue with search and discovery filters 2026-10-06 04:46:02 +02:00
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
805 changed files with 38052 additions and 875 deletions
+343
View File
@@ -1,5 +1,348 @@
# 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.238 — Forgerie
[Aptitude Forgerie](docs/forgerie-beta238.md) dans Arts et métiers : quatre
niveaux pour débloquer les enclumes et la table de forge, achat définitif.
Les réparations, noms, améliorations en netherite et ornements conservent
leurs règles natives. L'achat est contrôlé côté serveur, y compris pour les
postes portés sur la tête. Icône fournie et libellés FR/EN intégrés.
Build, 21 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.238](build/Sanctuary-beta.238.mrpack) vérifié, sans
publication ni mise à jour personnelle.
## beta.237 — Aptitudes acquises définitivement
[Aptitudes par thème et achats permanents](docs/aptitudes-beta237.md) : seuls
World Map et Mob Names sont activables/désactivables. Les dix autres aptitudes
restent utilisables après achat ; Enchantement et Alchimie coûtent chacun
quatre niveaux et débloquent l'accès personnel aux postes natifs. Icônes
fournies intégrées, libellés FR/EN. Les anciens choix de désactivation de ces
achats sont effacés sans toucher aux achats ni à l'XP. Disposition Pause
beta.235 intégrée, adaptée au choix de masquer la carte. `check build assemblePack`,
37 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.237](build/Sanctuary-beta.237.mrpack) vérifié ; aucune
publication ni mise à jour personnelle.
## beta.235 — Essai réversible du menu Pause
[Disposition adaptative](docs/pause-columns-beta235.md) : avant Atlas, Aventure
à gauche et Communauté à droite, avec Partie / Game juste dessous. Avec Atlas,
une colonne étroite à gauche, Communauté → Aventure → Game, carte au centre.
Bloc centré verticalement, libellés de navigation entiers et ordre clavier
adapté. Parcours Pause et Story Vulkan FR/EN GUI 2/3/4, `check build assemblePack`
et quatre GameTests ciblés réussis.
Le commit de disposition `a7bdfe6` peut être annulé indépendamment de la
livraison. [MRpack local beta.235](build/Sanctuary-beta.235.mrpack) vérifié,
sans publication ni mise à jour personnelle. Le rendu reste à apprécier.
## beta.234 — Story, recherche et filtres
[Premier menu Story](docs/story-beta234.md), sous Progression dans Aventure :
100 histoires inconnues et désactivées, recherche instantanée et filtre
Toutes/Découvertes/Inconnues, cadre de Discovery et retour à Pause. Saisie et
défilement conservés au redimensionnement ; trois boutons Aventure centrés
avec ou sans Atlas. [Guide](docs/story/README.md) et
[modèle Markdown](docs/story/TEMPLATE.md) pour les futurs récits personnels.
Textes et déblocages restent à intégrer. `check build assemblePack`, quatre
GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.234](build/Sanctuary-beta.234.mrpack) vérifié ; aucune
publication ni mise à jour d'installation personnelle.
## 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'
+140
View File
@@ -0,0 +1,140 @@
# APT-236 — aptitudes par thème et activation personnelle
La règle de désactivation décrite ici correspond à la livraison beta.236.
[Beta.237](aptitudes-beta237.md) la remplace : seuls World Map et Mob Names
restent désactivables, les autres aptitudes sont acquises définitivement.
Le chantier Pause beta.235 est intégré dans cette nouvelle livraison.
Branche `codex/progression-aptitudes`, base beta.234 (`f9c2ce8`). Le travail
parallèle du menu Pause occupe beta.235 ; ce lot réserve beta.236.
## Contrat avant implémentation
Progression regroupe les aptitudes en cinq thèmes : Survie (Swimming, Rest,
Food Knowledge), Inventaire et fabrication (Inventory Sorting, Catalogue),
Familiers (Active Bond, Familiar Study), Exploration (World Map, Mob Names,
Zoom), Arts et métiers (Enchantement, Alchimie). Le minage et la forgerie
restent hors de ce ticket.
Enchantement et Alchimie coûtent chacun 4 niveaux, comme les aptitudes actuelles.
L'usage personnel de la table d'enchantement et de l'alambic est verrouillé au
départ dans les mondes avec progression Sanctuary. Les menus natifs sont
refusés côté serveur tant que l'aptitude correspondante n'est pas acquise et
active, y compris via les blocs fonctionnels portés sur la tête. Les enclumes,
la table de forge, les équipements enchantés et les potions déjà obtenues
restent inchangés. Les alambics partagés et leur automatisation continuent
leur fonctionnement autonome : ce lot contrôle l'accès personnel au poste,
sans attribuer de propriétaire à un bloc ni arrêter une préparation lancée.
Les personnages existants avec progression Sanctuary doivent aussi acquérir
ces deux nouvelles aptitudes ; aucune acquisition gratuite n'est ajoutée.
Une aptitude achetée peut être activée ou désactivée sans coût et sans perdre
son achat. Food Knowledge reste permanent après apprentissage. Les compétences,
les prix et les découvertes ne changent pas. Les effets de gameplay sont
contrôlés côté serveur ; les aides d'interface utilisent le choix serveur.
Active Bond contrôle l'usage des pouvoirs de travail, pas les techniques de
combat, les pouvoirs passifs ou l'équipement du familier. Une action déjà
lancée se termine normalement ; les nouvelles activations sont refusées.
## Persistance et compatibilité
L'attachement additif `sanctuary:aptitude_preferences` conserve un entier de
0 à 2047 : un bit par aptitude désactivée, dans l'ordre immuable names, atlas,
sorting, swimming, resting, active_bond, familiar_study, catalogue, zoom,
enchanting, alchemy.
L'absence de cet attachement équivaut à zéro : toutes les aptitudes acquises
gardent leur comportement actuel. Il est copié à la mort et conservé au New
Game+. Deux attachements booléens additifs `sanctuary:enchanting_learned` et
`sanctuary:alchemy_learned` conservent leurs nouveaux achats ; leur absence
équivaut à false et ils sont copiés à la mort et conservés au New Game+.
Aucun schéma ou contenu des achats existants n'est réécrit. Ce contrat
est limité à cet attachement ; aucun monde personnel n'est ouvert ou modifié.
Une requête transmet la cible, le choix et le masque attendu ; le serveur
vérifie l'achat, la session, l'état du joueur et le masque courant. Un choix
inconnu, Food Knowledge, une aptitude non acquise ou une requête périmée est
refusé sans XP dépensée. Un état distinct est renvoyé avant le snapshot de
progression. La capacité réseau `aptitude_preferences_v1` exige les versions
compatibles sur client et serveur sans changer les paquets existants.
Désactiver Swimming interrompt le sprint aquatique ; désactiver Rest arrête
la récupération. World Map masque son entrée et refuse les nouvelles lectures
personnelles ; les relevés continuent d'être conservés et l'outil opérateur
reste disponible. Catalogue masque l'aide personnelle JEI et la consultation
complète de Discovery, sans toucher aux recettes ni aux découvertes.
Un ancien mod ignore ces trois nouveaux attachements et retrouve son ancien
accès aux postes. Il peut perdre les attachements inconnus en sauvegardant :
conserver une sauvegarde avant un retour arrière. Le schéma 9 de progression
et les preuves des aptitudes antérieures restent identiques.
## Vérifications
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=aptitudes236,progression,movement,sorting,recipes,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Première passe serveur : `check build assemblePack` réussi en 3 min 13 s,
**33 GameTests requis réussis**, dont cinq scénarios APT-236. Contrôles purs
du dépôt réussis. Le banc Atlas existant évite le conflit de génération du
chemin par défaut déjà documenté en beta.232. Monde de développement neuf.
Log : `build/aptitudes236-check-build-pack.log`.
- Onze choix indépendants, refus des requêtes inconnues, non acquises,
périmées et de Food Knowledge ; conservation exacte de l'XP et des preuves
d'achat. Achat à 3 niveaux refusé, à 4 accepté une seule fois.
- Aller-retour des nouveaux attachements dans le NBT joueur natif, copie à
la mort, conservation lors de la remise à zéro des compétences du cycle.
- Swimming arrête le sprint aquatique et interdit les flags de mouvement
directs ; Rest arrête la récupération ; tri falsifié et lectures Atlas
personnelles refusés. Réactivation du tri contrôlée avec la même requête.
Active Bond refuse une nouvelle activation avant toute planification.
- Table d'enchantement native refusée avant acquisition, ouverte ensuite ;
**une épée diamant est réellement enchantée**. Désactiver ferme le menu,
refuse sa réutilisation et son bouton d'enchantement sans dépense d'XP.
Alambic natif refusé puis ouvert, fermeture et invalidation à la désactivation,
ingrédients partagés conservés et réouverture après réactivation.
Parcours client final réussi en **52 s**, Minecraft 26.3, Vulkan confirmé par
MoltenVK 1.4.2 / Apple M1. Log : `build/aptitudes236-client-vulkan.log`, marqueur
`APTITUDES236_PASS` ; douze captures dans `build/aptitudes236-screenshots/`.
- FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes dans l'ordre, les douze
aptitudes présentes, Food Knowledge permanent et sans interrupteur.
- Achat des deux arts depuis les véritables boutons de Progression : huit
niveaux au total. **66 paires désactivation/réactivation** via les widgets
et le réseau, état serveur contrôlé, XP et progression finale identiques.
- Libellés d'état entièrement contenus dans les boutons ; défilement et
position conservée au redimensionnement 1344 × 1008 ; Terminé revient à
Pause. Une première passe a trouvé le libellé permanent trop large en GUI 4 :
retour à la ligne ajouté. Les interrupteurs affichent un état court et
expliquent leur action gratuite dans l'infobulle.
Les essais concernent les sauvegardes de développement. Pas de validation
Windows ou multijoueur distant. L'automatisation des alambics et les pouvoirs
déjà lancés restent les limites explicites du contrat ; la forgerie attend
son prochain ticket.
## Livraison locale
Build final `check build assemblePack` réussi en **2 min 24 s**, 136 tâches,
33/33 GameTests requis et contrôles purs réussis après les derniers réglages
d’interface. Log : `build/aptitudes236-check-build-pack-final.log`.
Versions du mod, du pack et du manifeste alignées sur **beta.236**.
[MRpack local](../build/Sanctuary-beta.236.mrpack) vérifié : 12797046 octets,
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
identique au build final, sans classe de test ni monde ni module de laboratoire.
Les deux PNG fournis sont identiques octet pour octet aux originaux dans le JAR.
Reçu : `build/aptitudes236-artifact.json`.
- SHA-256 JAR : `21a4411ebed223a1d9f9d204dc9d7300af0d5b946416e39bb138f5c575bf5847`.
- SHA-256 MRpack : `9d3eca559805d451e8c653cca1d59186ae46e3c6964147b9fc072b9da832f921`.
La branche reste indépendante du chantier Pause beta.235 ; leur intégration
reste à effectuer. Aucune publication distante, mise à jour du canal packwiz
ou synchronisation d’instance personnelle.
+99
View File
@@ -0,0 +1,99 @@
# APT-237 — achats permanents et intégration du menu Pause
Branche `codex/progression-aptitudes`. Intégration des commits beta.235
`a7bdfe6` et `00d175c`, avec les aptitudes beta.236 `1c8e643`.
## Contrat avant implémentation
Seuls World Map et Mob Names disposent d'un interrupteur gratuit après achat.
Swimming, Rest, Inventory Sorting, Catalogue, Active Bond, Familiar Study,
Zoom, Enchantement, Alchimie et Food Knowledge restent acquis et utilisables
sans désactivation. Les prix et les conditions d'achat restent identiques.
Enchantement et Alchimie exigent toujours leur achat avant l'accès personnel
au poste natif. Les achats, l'XP et les preuves existantes sont conservés.
Le menu Pause reprend la disposition beta.235 : deux colonnes Aventure et
Communauté avant World Map, une colonne à gauche avec la carte. Le choix
personnel de masquer World Map suit aussi cette disposition.
## Contrat de migration des préférences beta.236
L'identifiant `sanctuary:aptitude_preferences`, son codec entier 0–2047 et
l'ordre historique des onze bits restent stables. Les bits 0 (Mob Names) et
1 (World Map) restent effectifs. Les bits 2 à 10 deviennent inertes et sont
retirés de cet attachement lors de sa prochaine synchronisation serveur.
Ainsi un ancien masque 2047 devient 3 : les deux aides restent masquées et
les neuf autres aptitudes achetées retrouvent automatiquement leur usage.
Le chargement accepte encore les anciennes valeurs ; les lectures et effets
ignorent immédiatement leurs bits désormais inertes. Aucune preuve d'achat,
aucun format de progression, aucun monde ou terrain n'est réécrit.
Les requêtes de désactivation des aptitudes permanentes sont refusées même
si elles proviennent d'un ancien client. Les paquets v1 restent inchangés ;
une capacité additive `sanctuary:aptitude_permanent_v1` impose un client avec
cette règle côté serveur. Les nouvelles interfaces sont traduites FR/EN.
Le retour à une ancienne version nécessite comme auparavant une sauvegarde,
car cette version ne peut restaurer les anciennes désactivations effacées.
## Vérification et livraison
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=aptitudes236,menus,progression,movement,sorting,recipes,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Les suites APT-236 sont actualisées pour vérifier la nouvelle règle et la
migration ; leurs noms Gradle restent stables. `check build assemblePack`
réussi en **2 min 34 s**, **37 GameTests requis réussis**, dont six scénarios
aptitudes. Contrôles purs du dépôt réussis. Le banc Atlas évite le chemin de
génération par défaut déjà documenté en beta.232. Log :
`build/aptitudes237-check-build-pack.log`.
- Les deux choix personnels sont gratuits ; refus des requêtes périmées,
inconnues, non acquises et des désactivations d'aptitudes permanentes.
Prix des nouveaux achats vérifié à trois/quatre niveaux, achat unique.
- Ancien masque 2047 rechargé dans le NBT natif, interprété en 3 puis
normalisé sans changement d'XP ni de preuve d'achat. Conservation des
choix et des achats à la mort et au New Game+.
- Avec d'anciens bits désactivés : sprint aquatique, Rest et tri restent
utilisables ; Active Bond suit son contrôle normal de familier équipé.
World Map conserve son contrôle d'accès et sa réactivation gratuite.
- Enchantement et Alchimie refusés avant achat, puis disponibles lors des
visites suivantes. Une épée diamant est réellement enchantée. Une requête
de désactivation refusée ne ferme ni n'invalide les menus acquis ; ingrédients
partagés de l'alambic conservés. Accès vanilla hors progression conservé.
Parcours client réussi en **32 s**, Minecraft 26.3, Vulkan / MoltenVK 1.4.2
sur Apple M1. FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes, douze
aptitudes, dix achats permanents, **12 paires désactivation/réactivation**
réelles des deux aides. Deux achats des arts via les boutons natifs de
Progression, huit niveaux au total. Libellés contenus dans leurs boutons,
XP et preuves conservées, défilement conservé au redimensionnement et retour
Terminé vers Pause.
La même suite réutilise les assertions Pause du collègue : disposition,
ordre clavier, marges, titres et libellés de navigation vérifiés après achat
de World Map, avec son choix personnel désactivé puis activé. Les deux
colonnes et la colonne près de la carte sont conservées. Les captures de
Pause portent sur la disposition ; les relevés et fils commencent à charger.
Log `build/aptitudes237-client-vulkan.log`, marqueur `APTITUDES237_PASS` ;
**24 captures** dans `build/aptitudes237-screenshots/`. Inspection des captures
Arts FR GUI 3, Pause sans carte FR GUI 4 et Pause avec carte EN GUI 3.
Versions alignées sur **beta.237**. [MRpack local](../build/Sanctuary-beta.237.mrpack)
vérifié : 12797901 octets, intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5,
un seul JAR Sanctuary identique au build final, sans tests ni monde ni module
de laboratoire. Les deux icônes fournies restent identiques octet pour octet
aux originaux. Reçu `build/aptitudes237-artifact.json`.
- SHA-256 JAR : `7ac741c6a15b8ab05a909cf0132c9d605b863723c0742a92d8b60c741fd760a6`.
- SHA-256 MRpack : `7635e9e7a425cbd7a7f6d020f9bb2bcced8cc1287967093346295e4b4b2250e9`.
Les anciens artefacts beta.236 sont conservés. Tests dans des mondes de
développement neufs ; Windows et multijoueur distant non testés. Les
alambics autonomes et pouvoirs déjà lancés gardent le contrat beta.236.
La forgerie, les enclumes et la table de forge attendent leur prochain ticket.
Aucune publication, avancée du canal ni synchronisation d'installation personnelle.
-299
View File
@@ -1,299 +0,0 @@
# Audit du regroupement de Sanctuary beta.203
Audit du 4 octobre 2026, demandé avant toute refonte. **Le regroupement est
réalisable, mais Sanctuary Test contient maintenant une partie du jeu livré,
et JEI constitue un moteur conséquent.** La voie recommandée est de conserver
le comportement de la beta.203, absorber Demeure et les fonctionnalités du
laboratoire, puis traiter le catalogue comme un chantier distinct.
Cette livraison contient une sauvegarde et un audit. Aucun regroupement de
code, changement de génération, migration de monde ou déploiement n'a été
effectué. Les travaux proposés ci-dessous restent à réaliser.
## État de référence et sauvegarde
La référence est le commit `454d9a5c08717af807f233759d3a96d639a39d44`, branche
`codex/north-fracture-beta203`, dans
`/Users/koka/.codex/worktrees/storyquest-beta173/sanctuary-beta`.
Son état Git était propre. Les propriétés du mod et du pack déclarent
`beta.203`, pour Minecraft 26.3, Java 25, Fabric Loader 0.19.5 et Fabric API
0.160.5+26.3.
Le dossier principal ouvert au début de l'audit était encore sur `main`, en
beta.168. La branche 203 possède 40 commits absents de ce `main` local. Cela
explique le décalage initial ; le présent audit porte bien sur la 203.
Sauvegarde locale :
`/Users/koka/Documents/sanctuary-backups/beta.203-before-single-mod-20261004-225340/`.
- Branche de sauvegarde : `codex/snapshot-beta203-before-single-mod-20261004-225340`.
- `sanctuary-all-refs.bundle` : historique Git et références commises présentes
au moment de la sauvegarde, dont la 203 et le `main` local.
- `sanctuary-beta.203-sources.tar.gz` : fichiers suivis de la 203.
- `jei-upstream-aae2dfc.tar.gz` : sources amont exactes nécessaires au port JEI ;
le patch et le script de préparation sont dans les sources Sanctuary.
- `artifacts/` : JAR existants de la 203, JAR de sources, MRpack Test, template
graphique et guide. Le JAR Sanctuary contient également JEI et MariaDB.
- Reçu de livraison et deux journaux de validation 203 ; empreintes SHA-256
dans `manifest.json`, procédure dans `RESTORE.txt`.
Les 16 fichiers du manifeste ont été contrôlés. Le bundle a été vérifié, puis
cloné dans un répertoire neuf : commit attendu retrouvé, copie restaurée
propre. Les 4 628 fichiers de l'archive source ont également été comparés
octet pour octet à la copie 203 d'origine. Le MRpack sauvegardé a pour SHA-256
`ba160b4134c0b9dbe402e1834bdbec4f5561bfd6bd9dc147590dd76ee30dbf4f`, identique
au [reçu documenté de la 203](north-fracture-beta203.md).
**Limite de sauvegarde :** la tentative de copie de la branche vers Gitea a
échoué faute d'authentification HTTPS. Aucune sauvegarde distante nouvelle
n'est confirmée. Cette sauvegarde locale du projet ne contient pas les mondes,
réglages personnels, caches Gradle, essais ignorés ni changements non commis
des autres worktrees. Elle ne constitue pas une installation complète hors ligne.
## Inventaire mesuré
Comptage des fichiers Java et de leurs lignes physiques, commentaires et
lignes vides inclus. Les sources JEI sont celles préparées au commit fixé,
dans les six répertoires effectivement compilés par son build.
| Ensemble | Java livré | Lignes Java | Responsabilité actuelle |
| --- | ---: | ---: | --- |
| Sanctuary | 963 fichiers | 84 353 | Terrain historique, progression, inventaires, recettes côté serveur, Atlas, communauté, rendu, expansions |
| Demeure | 4 fichiers | 347 | Empreintes du vivant, activité et persistance pour l'Atlas |
| Sanctuary Test | 105 fichiers | 8 775 | Nouveau terrain, palais et cavernes, ancres, adaptation des expansions, outils de labo |
| JEI embarqué | 924 fichiers | 69 104 | Recherche, catalogue, écrans de recettes, favoris, catégories, synchronisation et transferts |
Sanctuary possède également 381 fichiers Java de tests, soit 60 380 lignes,
répartis entre tests purs, GameTests serveur et tests client. Demeure et le
module Test ont chacun un fichier de test supplémentaire. Le volume de tests
ne prouve pas leur exécution sur chaque livraison.
Les configurations déclarent 286 mixins Sanctuary, un Demeure, sept Sanctuary
Test et sept JEI. Ces injections dans Minecraft rendent l'ordre
d'initialisation et les interactions client/serveur sensibles aux déplacements.
Le graphe de construction comprend quatre sous-projets. Sanctuary embarque
Demeure, JEI et MariaDB dans son JAR ; JEI embarque deux bibliothèques d'indexation.
Le MRpack 203 contient deux JAR de jeu au premier niveau, Sanctuary et Sanctuary
Test, et télécharge Fabric API. L'inspection des archives confirme que les
identités Fabric `demeure`, `jei` et `sanctuary_test` existent encore réellement.
Sources : [settings.gradle](../settings.gradle),
[dépendances Sanctuary](../mods/sanctuary/build.gradle),
[manifeste Sanctuary](../mods/sanctuary/src/main/resources/fabric.mod.json),
[port JEI](../mods/jei/build.gradle).
## Les points qui rendent une fusion directe dangereuse
### Sanctuary Test porte le terrain utilisé dans la 203
Ce module n'est plus seulement un moyen de sauter l'introduction dans un monde
plat. Il fournit 46 presets, 44 paramètres de bruit, 93 biomes et 13
enregistrements de codecs. Son adaptateur `LabExpansions199` branche le terrain
récent sur le journal, les réservations et la file de génération de Sanctuary.
Il existe deux fichiers différents à l'emplacement
`data/sanctuary/worldgen/world_preset/sanctuary.json`. Celui de Sanctuary
sélectionne `sanctuary:island_v24`. Celui du module Test sélectionne le terrain
natif du laboratoire, avec `sanctuary_test:ascent_1600` et les biomes du labo.
Une copie avec écrasement implicite pourrait donc changer le terrain proposé
par le bouton Sanctuary. Le tag des presets publics présente aussi une collision.
Les classes `NorthTerrain200` à `NorthTerrain203`, déjà dans Sanctuary,
référencent des biomes `sanctuary_test:*` fournis par l'autre module : la frontière
des fonctionnalités ne suit pas celle des sous-projets Gradle.
**Conséquence :** transférer explicitement le terrain et ses ressources, choisir
un seul preset public, conserver les anciennes clés de registre, puis déplacer
les outils de mesure vers des sources de développement. Le nom du mod peut
disparaître sans renommer immédiatement tous les identifiants de ses ressources.
Les numéros historiques ne suffisent pas à identifier du code supprimable.
Sources : [initialisation du labo](../mods/sanctuary-test/src/main/java/fr/koka/sanctuarytest/WorldgenLab.java),
[adaptateur des expansions](../mods/sanctuary-test/src/main/java/fr/koka/sanctuarytest/LabExpansions199.java),
[preset livré par le labo](../mods/sanctuary-test/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json),
[biomes du Nord 203](../mods/sanctuary/src/main/java/fr/koka/sanctuary/expansion/NorthTerrain203.java).
### Les raccourcis du labo ne doivent pas devenir les règles ordinaires
`QuickTestMod` active le pont pour mondes plats et enregistre plusieurs outils
de test. Son paquet de configuration peut demander de sauter l'introduction
pour les mondes du labo. `QuickTestClient` intervient dans la création de monde.
`SkySurvey175` exige même un conteneur Fabric nommé `sanctuary_test`.
Absorber aveuglément ces points d'entrée étendrait les comportements de labo
au mod principal, ou laisserait des outils dépendre d'une identité supprimée.
Il faut séparer les services nécessaires au jeu, les options explicites de
développement et les tests, puis n'enregistrer chaque événement qu'une fois.
Sources : [QuickTestMod](../mods/sanctuary-test/src/main/java/fr/koka/sanctuarytest/QuickTestMod.java),
[QuickTestClient](../mods/sanctuary-test/src/main/java/fr/koka/sanctuarytest/QuickTestClient.java),
[activation du gameplay](../mods/sanctuary/src/main/java/fr/koka/sanctuary/progression/SanctuaryGameplay.java).
### Demeure est petit mais ses données doivent garder leur adresse
Seuls deux fichiers Java de production Sanctuary référencent directement
son package : l'initialisation et le paquet Atlas. L'absorption est donc
relativement simple : service interne, événements, mixin de placement et tests.
Il faut conserver `data/demeure/footprints-v1.json`, son schéma 1, la graine et
les protections contre la corruption ou le remplacement externe. Renommer
ce chemin pendant la fusion ferait apparaître un historique vide. Préserver
aussi le filtre d'éligibilité fixé par Sanctuary et éviter un double comptage
si l'ancien JAR Demeure reste installé.
Sources : [service Demeure](../mods/demeure/src/main/java/fr/koka/demeure/DemeureService.java),
[écriture atomique](../mods/demeure/src/main/java/fr/koka/demeure/AtomicJson.java).
### JEI est le principal choix de conception
Sanctuary ne référence directement les classes JEI que dans deux fichiers
de production, `RecipeClient` et `SanctuaryJeiPlugin`. Cette frontière limitée
est favorable à un remplacement. Elle masque toutefois un moteur de 924
fichiers Java, avec un patch local de 1 491 lignes touchant 41 fichiers.
JEI fournit aussi des services serveur : transferts d'ingrédients,
sérialiseurs et synchronisation des recettes. Son démarrage dépend des points
d'entrée Fabric, de mixins, d'un access widener et de services Java. Effacer
son manifeste ne constitue donc pas une intégration fonctionnelle.
Deux trajectoires sont possibles :
- **Internaliser le moteur dérivé de JEI.** Faire disparaître le mod distinct,
adapter son démarrage et conserver ses comportements. Risque fonctionnel
plus limité, mais environ 69 000 lignes tierces restent à maintenir, avec
leurs crédits, licences, ressources et conventions.
- **Construire le catalogue Sanctuary.** Remplacer progressivement le moteur
derrière une interface interne. C'est le meilleur moyen de réduire la
dépendance à JEI, avec davantage de développement et de validation.
La seconde trajectoire doit couvrir la recherche, recettes/usages, favoris,
historique, fluides, cuisson, alchimie et autres catégories réellement utilisées,
les recettes de datapacks, les interfaces de conteneurs et les transferts avec
inventaire étendu. Les règles d'apprentissage, recettes cachées, découvertes et
révocation opérateur restent gouvernées par les services serveur Sanctuary.
L'interface native seule ne suffit pas à remplacer ce contrat.
Sources : [RecipeClient](../mods/sanctuary/src/main/java/fr/koka/sanctuary/client/RecipeClient.java),
[pont JEI](../mods/sanctuary/src/main/java/fr/koka/sanctuary/client/SanctuaryJeiPlugin.java),
[contrat des recettes](recipes-jei-beta024.md),
[provenance JEI](../mods/jei/PROVENANCE.md).
### Le build et la distribution imposent encore plusieurs mods
`scripts/pack.py` exige explicitement les JAR imbriqués Demeure et JEI,
leurs versions, licences et empreintes. `scripts/test_pack.py` exige un JAR
`sanctuary_test`. Les scripts de lancement dépendent de ses tâches Gradle
et de ses presets. Tous ces contrats devront évoluer dans les mêmes tickets
que les modules concernés, en gardant les vérifications d'intégrité.
Un seul mod de jeu Sanctuary peut continuer à utiliser Fabric Loader,
Fabric API et des bibliothèques techniques. L'exigence plus stricte « aucune
autre entrée Fabric, même technique » serait un périmètre différent : Fabric
API et certaines bibliothèques imbriquées possèdent elles-mêmes des métadonnées.
Elle n'est pas nécessaire pour réunir les fonctionnalités demandées.
Sources : [assemblage du pack](../scripts/pack.py),
[assemblage Test](../scripts/test_pack.py),
[lancement du terrain](../scripts/worldgen_lab.py).
## Nettoyage utile du dépôt
Le découpage interne de Sanctuary existe déjà : progression, inventaire,
recettes, communauté, rendu, monde, expansions, etc. Le conserver évite qu'un
mod unique devienne une classe unique. Le terrain représente à lui seul
35 599 lignes Java dans Sanctuary, avec des générations historiques enregistrées.
Les premiers gains seraient de clarifier la référence de développement,
d'extraire les paramètres et tâches répétitifs du `build.gradle` Sanctuary
(1 355 lignes), et de transformer le README de 2 881 lignes en porte d'entrée
vers un historique séparé. Certains passages annoncent toujours beta.166 et
le README Demeure indique encore 26.3-pre-2 ; les propriétés de build et le
ticket 203 sont les références utilisées ici.
Il y avait 48 worktrees avant celui de cet audit. Ce nombre augmente le risque
de travailler sur une ancienne version, comme l'a montré le décalage 168/203.
Les archiver demandera de distinguer les branches terminées des travaux encore
actifs. Aucun de ces worktrees n'a été supprimé par cet audit. Les gros dossiers
`build/` ignorés ne sont pas assimilés à des sources mortes à effacer.
## Découpage proposé en tickets vérifiables
| Ticket proposé | Résultat attendu | Vérification déterminante |
| --- | --- | --- |
| UNI 01 Référence et contrats | Branche issue de 203, inventaire des clés persistantes, fonctions de catalogue et ressources en collision | Reconstruction depuis la sauvegarde, référence de tests documentée, exemplaires de mondes de développement |
| UNI 02 Demeure interne | Plus de mod Demeure séparé ; empreintes et Atlas inchangés | Comptage unique des événements, relecture du fichier schéma 1, refus de corruption |
| UNI 03 Terrain dans Sanctuary | Terrain 203, Nord, palais et relais disponibles avec Sanctuary seul | Création Small/Medium/Large, graines 0/42/4736390610738281858, reprise d'expansion et rechargement |
| UNI 04 Outils de développement séparés | Plus de mod Sanctuary Test ; tâches de labo explicites et sources de test | Parcours normal sans raccourcis d'introduction, parcours labo opt-in, serveur dédié et client Vulkan |
| UNI 05 Catalogue interne | Interface indépendante de JEI, puis internalisation ou remplacement selon le périmètre retenu | Recherche, usages, favoris, filtres de découverte et transferts réels en solo et à deux |
| UNI 06 Distribution et documentation | Un artefact de jeu Sanctuary, installation existante débarrassée des anciens JAR, documentation actuelle | `check build`, `assemblePack`, contenu des archives et essai d'installation isolé |
Chaque ticket reçoit une branche `codex/<sujet>`. Les livraisons binaires
incrémentent la prochaine version disponible ; cet audit documentaire reste
sur beta.203. Les étapes peuvent être rapprochées, mais chacune doit laisser
un résultat vérifiable avant la suivante.
Pour les mondes existants, établir d'abord un contrat de compatibilité : garder
les codecs et noms `sanctuary:*` et `sanctuary_test:*`, les schémas et chemins,
notamment `sanctuary-lab-expansions<révision>`. Vérifier sur des copies de
développement les constructions, progression, recettes, empreintes et expansions
en attente. Un changement de nom public ou de package Java ne justifie pas
une régénération de chunks. Aucun essai sur un monde personnel n'est requis
pour commencer ces tickets.
## Difficulté estimée
Estimation de travail concentré pour une personne connaissant le dépôt,
incluant vérification et corrections ; ce ne sont pas des durées mesurées.
| Périmètre | Ordre de grandeur | Incertitude principale |
| --- | --- | --- |
| Demeure absorbé | 0,5 à 1 jour | Persistance et absence de double enregistrement |
| Terrain et fonctions Test absorbés, outils séparés | 3 à 5 jours | Anciennes clés, ressources en collision, chargement et reprise |
| JEI internalisé avec son moteur conservé | 2 à 4 jours | Initialisation, services, ressources et transferts |
| Catalogue propre remplaçant les usages actuels de JEI | 10 à 20 jours | Étendue exacte des catégories, confort d'usage et parité multijoueur |
Avec la remise au propre du build et de la distribution, compter **environ
une à deux semaines pour une unification conservatrice**, et **trois à cinq
semaines pour un ensemble nettoyé avec remplacement du catalogue**. Une
équivalence avec toutes les intégrations tierces possibles de JEI dépasserait
ce périmètre. La phase UNI 01 doit resserrer ces estimations.
Ma recommandation est d'absorber Demeure puis le terrain Test, en conservant
les formats, et d'encapsuler immédiatement le catalogue avant de le remplacer.
Renommer tous les identifiants et réécrire la génération dans cette même passe
augmenterait le risque sans être nécessaire à l'objectif d'un mod unique.
## Vérifications et limites de cet audit
Sauvegarde et restauration vérifiées, archives existantes inspectées, graphe de
dépendances et ressources en collision contrôlés. La copie d'audit possède sa
branche `codex/audit-single-mod-beta203`, distincte des branches de jeu.
Une exécution ciblée a été réalisée dans cette copie isolée :
```sh
./gradlew --no-daemon check build \
-PsanctuaryFocusedTests=demeure,recipes,operator,realtime \
-PsanctuaryAtlasOnly=true
```
**Résultat : BUILD SUCCESSFUL en 2 min 57 s ; 140 tâches, dont 139 exécutées,
et 15/15 GameTests requis réussis.** Les tests numériques rattachés à `check`,
la compilation des quatre modules, les contrôles du manifeste pack et des
ressources ont également passé. Journal : `build/audit-beta203-check-build.log`.
Le cache des sources JEI a été copié depuis le worktree 203, puis contrôlé
par le script de préparation ; ce n'est pas une reconstruction sans caches.
L'assemblage des packs n'a pas été relancé dans cet audit : les archives
203 existantes ont été inspectées et sauvegardées.
La [livraison 203](north-fracture-beta203.md) documente sept GameTests ciblés,
les tests numériques et un essai natif North Small/42. La suite historique
complète n'est plus validée depuis la [limite décrite en 200](north-expansion-beta200.md),
où une exécution a été interrompue pendant la préparation d'un test hydrologique.
Cet arrêt ne démontre pas un interblocage. Les succès antérieurs sur 265 tests
ne constituent pas une validation de la 203.
Cet audit n'est pas un test visuel, un essai Windows, une validation des
performances ou une preuve de migration des sauvegardes. Aucun client graphique
n'a été lancé ; les futures validations graphiques ciblent uniquement Vulkan.
+38
View File
@@ -1,5 +1,43 @@
# Backlog Sanctuary
## APT-238 — Forgerie — livré localement
Branche `codex/forgerie-beta238`, base beta.237. Aptitude permanente des Arts
et métiers ; quatre niveaux pour les trois variantes d'enclume et la table
de forge, y compris les postes portés. [Contrat et persistance](forgerie-beta238.md).
Réparations, noms, améliorations et ornements gardent leurs règles natives.
Build, 21 GameTests requis, Vulkan FR/EN GUI 2/3/4 et MRpack vérifiés.
## APT-237 — achats permanents et intégration Pause — livré localement
Branche `codex/progression-aptitudes`, travaux beta.235 et beta.236 réunis.
Cinq thèmes, douze aptitudes ; seuls World Map et Mob Names ont un interrupteur.
Enchantement et Alchimie restent à acheter avant l'accès personnel au poste.
[Contrat et migration](aptitudes-beta237.md) des préférences antérieures,
avec conservation des achats et de l'XP. Build, 37 GameTests requis,
Vulkan FR/EN GUI 2/3/4 et MRpack local vérifiés. La forgerie attend ses précisions.
## PAUSE-235 — groupes côte à côte avant Atlas — essai livré localement
Branche `codex/pause-columns-beta235`, base beta.234. Avant Atlas : Aventure à
gauche, Communauté à droite et Game juste dessous, bloc centré. Avec Atlas :
une colonne étroite à gauche dans l'ordre Communauté → Aventure → Game,
carte centrale et fils à droite. [Contrat et vérifications](pause-columns-beta235.md),
parcours Vulkan Pause et Story FR/EN GUI 2/3/4, build, quatre GameTests ciblés
et MRpack local vérifiés ; rendu à apprécier.
Commit de disposition `a7bdfe6` séparé des métadonnées pour permettre son retour.
## STORY-234 — recueil personnel avec recherche — livré localement
Branche `codex/story-beta234`, base beta.233. Story sous Progression dans
Aventure ; 100 histoires inconnues, barre de recherche et filtre de découverte.
Cadre commun avec Discovery, état de recherche et défilement conservés au
redimensionnement, accès avant et après Atlas. [Résultat et vérifications](story-beta234.md) :
build, quatre GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[Modèle Markdown](story/TEMPLATE.md) pour les textes de l'auteur.
Les récits et leurs conditions de découverte restent des tickets suivants ;
aucun moteur de quête, format persistant ou déploiement ajouté.
## WG-RESTORE-191 — annuler les plateaux et leurs affleurements
La visite beta.190 ne valide pas le relief corrigé. [Retour exact à la base
+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.
+95
View File
@@ -0,0 +1,95 @@
# APT-238 — Forgerie
Branche `codex/forgerie-beta238`, base beta.237 (`5dee981`).
## Contrat avant implémentation
Une aptitude Forgerie est ajoutée dans Arts et métiers de Progression, avec
l'icône `aptitudes_forgerie.png` fournie. Comme Enchantement et Alchimie,
elle coûte quatre niveaux, est achetée une seule fois et reste acquise
sans interrupteur. Elle débloque l'usage personnel de toutes les variantes
d'enclume et de la table de forge. Avant l'achat, les menus natifs de ces
postes sont refusés côté serveur, y compris les postes fonctionnels portés
sur la tête. Les réparations, noms, améliorations et ornements natifs restent
fonctionnels après achat. Équipements déjà obtenus et recettes conservés.
La meule, les fours et les autres postes restent sous leurs règles actuelles.
Les deux aides World Map / Mob Names restent les seuls choix désactivables.
Les interfaces ajoutées sont traduites en français et en anglais.
## Persistance et compatibilité
L'attachement booléen additif `sanctuary:smithing_learned` conserve l'achat.
Absent : false. Copie à la mort et conservation au New Game+. Aucun schéma,
aucune preuve ni aucun attachement existant n'est réécrit. Les personnages
existants avec progression doivent aussi acquérir Forgerie. Les joueurs
hors progression Sanctuary gardent l'accès vanilla. Aucun monde personnel,
aucune génération ni aucun terrain n'est modifié.
La capacité `sanctuary:smithing_v1` et l'état `sanctuary:smithing_state_v1`
transmettent l'achat sans modifier
les paquets v1 existants. Le serveur exige un client compatible et contrôle
la session, le joueur, la révision et les niveaux avant l'achat. Un retour
à une version ancienne peut perdre cet attachement inconnu à la sauvegarde ;
conserver une sauvegarde avant ce retour.
## Vérifications et livraison
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=forgerie238,aptitudes236,headblocks \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
`check build assemblePack` réussi en **2 min 44 s**, **21 GameTests requis**
réussis : cinq nouveaux scénarios Forgerie, six scénarios d'aptitudes et dix
scénarios de postes portés. Contrôles purs du dépôt réussis. Le banc Atlas
évite le chemin de génération par défaut déjà documenté en beta.232.
Log `build/forgerie238-check-build-pack.log`.
- Achat refusé avec trois niveaux ou une révision périmée ; quatre niveaux
dépensés une seule fois. Autres achats et preuves inchangés. Requête de
désactivation refusée ; conservation native NBT, à la mort et au New Game+.
- Trois variantes d'enclume refusées avant achat puis ouvertes après achat.
**Réparation et renommage réels d'une épée diamant** sur chacune ; XP et
diamant consommés selon le coût natif du menu.
- Table de forge refusée avant achat puis ouverte : **épée netherite et
plastron diamant avec ornement Sentry réellement produits**, entrées
consommées une fois, coût d'XP natif conservé et accès aux visites suivantes.
- Menus construits en dehors de l'ouverture normale : validité et prise de
résultat refusées avant acquisition, sans dépense ni perte d'entrées.
Joueurs sans progression Sanctuary autorisés comme en vanilla.
- Enclumes et table de forge sur la tête : contrôle de l'achat du visiteur,
indépendamment du porteur. La fixture générale des postes natifs possède
explicitement les trois aptitudes des Arts avant de tester leurs menus.
Sources et bytecode du **Minecraft 26.3 local** vérifiés pour les méthodes
héritées de `ItemCombinerMenu`, l'override de prise d'`AnvilMenu` et les recettes
natives de forge. Aucun changement de recette ni de dépendance externe.
Parcours graphique en **34 s**, Vulkan / MoltenVK 1.4.2 sur Apple M1,
FR/EN GUI 2/3/4. La suite d'aptitudes existante contrôle désormais treize
aptitudes, dont onze achats permanents. Trois Arts achetés depuis les boutons
natifs : douze niveaux au total, Forgerie synchronisée depuis le serveur et
présentée comme acquise sans interrupteur. Deux seules aides désactivables,
XP et progression conservées lors de douze paires de changement, disposition
Pause intégrée, défilement et retour Terminé conservés. Log
`build/forgerie238-client-vulkan.log`, marqueur `FORGERIE238_PASS`.
Vingt-quatre captures dans `build/forgerie238-screenshots/` ; Arts FR GUI 3
et EN GUI 4 inspectés, texte et icône contenus dans le cadre.
Versions du mod, du pack et du manifeste alignées sur **beta.238**.
[MRpack local](../build/Sanctuary-beta.238.mrpack) vérifié : 12802487 octets,
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
identique au build final, sans classe de test, monde ou module de laboratoire.
Les trois icônes fournies des Arts sont identiques octet pour octet aux
originaux dans le JAR. Reçu `build/forgerie238-artifact.json`.
- SHA-256 JAR : `e29ea6f7f56c57290d0307076f641f2e714ec72a80be858a5fd9c5f08e27eb8e`.
- SHA-256 MRpack : `2fbd605acf555dd8b192c0c0ad7171414a13f5820e5d440ba54debcd4cb897a7`.
Tests dans des mondes de développement neufs. Pas de validation Windows ni
multijoueur distant. Les sauvegardes et installations personnelles restent
intactes ; aucune publication ni avancée du canal packwiz.
+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.
+97
View File
@@ -0,0 +1,97 @@
# PAUSE-235 — Aventure et Communauté côte à côte
Branche d'essai `codex/pause-columns-beta235`, base beta.234, commit `f9c2ce8`,
Minecraft 26.3. Le rendu reste à apprécier par le créateur.
## Disposition et réversibilité
Avant Atlas, Aventure et Communauté occupent deux colonnes au même niveau :
Discovery, Progression et Story à gauche ; Habitant, Faction et Battle à droite.
Partie / Game suit juste dessous avec Options, World Options et la sortie
native. L'ensemble est centré verticalement dans le corps de Pause, avec de
l'espace sous Game ; Reprendre la partie reste dans le pied de page.
Sans Atlas, ce bloc est centré entre Gazette à gauche et Notice Board à droite.
Avec Atlas, une seule colonne étroite est serrée à gauche : Communauté, puis
Aventure, puis Partie / Game, selon l'ordre confirmé par le créateur. La carte
garde la région centrale et les deux fils communautaires restent à droite.
Le bloc complet reste centré verticalement. La largeur minimale des boutons
de navigation tient compte des libellés localisés et de leur marge native.
Aux petites hauteurs, boutons et espacements se compactent. Tab suit l'ordre
visuel : par rangée de gauche à droite avant Atlas, puis de haut en bas avec
Atlas. Les libellés FR/EN existants sont réutilisés.
Le changement de disposition et les assertions correspondantes sont isolés
dans le commit `a7bdfe6`, séparé de la livraison et de ses métadonnées. La branche
`codex/story-beta234` conserve la disposition précédente. Un retour s'effectue
avec `git revert a7bdfe6` ; une éventuelle nouvelle
livraison doit prendre le numéro beta suivant, sans diminuer le compteur.
L'application inverse du patch a été vérifiée avec `git apply --reverse --check`,
sans modifier les fichiers de travail.
Ce ticket ne change ni génération, sauvegarde, données communautaires, protocole,
conditions de progression ou contenus Story. Aucune installation personnelle
ou publication n'est demandée.
## Vérifications
```sh
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
```
Le parcours Pause final réussit en 1 min 22 s (`PAUSE233_PASS`), dans un monde
plat neuf de développement, Survie sans commandes. FR/EN, GUI 2/3/4, fenêtre
1280 × 960, avant et après Atlas : six actions Sanctuary, alignement et absence
de chevauchement, largeur des libellés avec la marge native, ordre de Tab et
Game sous les groupes, avec de l'espace avant le pied. Le test attend désormais
un créneau de requête communautaire pour son amorçage et envoie le code de
raccourci natif de Tab, requis par Minecraft 26.3.
Le parcours communautaire existant passe également : publication et photo,
retours des sélecteurs, brouillons, édition d'une ancienne catégorie, inventaire
réel conservé et aucun changement du fichier communautaire pendant les lectures.
La configuration temporaire est restaurée à la fin du banc.
Log : `build/pause235-client-vulkan-final.log`. 38 captures finales copiées dans
`build/pause235-screenshots/`. Captures inspectées : sans Atlas FR/4, avec Atlas
FR/2 et EN/4. Vulkan confirmé : MoltenVK 1.4.2 sur Apple M1.
Le parcours Story final réussit en 29 s (`STORY234_PASS`), avec les mêmes
langues, GUI et états Atlas. Accès depuis les deux dispositions de Pause,
100 entrées désactivées, recherche/filtres, saisie continue et focus, molette,
redimensionnement à 1344 × 1008 et retours Échap / Terminé vérifiés. Le record
de progression serveur ne change pas. Log :
`build/pause235-story-client-vulkan-final.log` ; 21 captures copiées dans
`build/pause235-story-screenshots/`.
Aux GUI étroits, les longs libellés natifs des options/sortie et les titres des
fils gardent leur défilement ou découpage existant ; les boutons de navigation
Sanctuary montrent leur libellé entier. Les infobulles des actions Game sont
conservées. Aucun essai OpenGL, Windows ou monde personnel n'est revendiqué.
Le contrôle `check build assemblePack` réussit en 3 min 1 s : quatre GameTests
requis et contrôles purs du dépôt, dont les quatre contrôles communautaires
fichier/photos/demandes/recherche. Log : `build/pause235-check-build-pack.log`.
Le banc menus conserve sa sélection Atlas existante ; le conflit de générateur
sur le chemin natif par défaut, documenté en beta.232, reste hors de ce ticket.
## Livraison locale
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.235.
Le commit de livraison conserve ces métadonnées et cette documentation séparées
du commit de disposition.
Export [Sanctuary-beta.235.mrpack](../build/Sanctuary-beta.235.mrpack),
12 776 222 octets : intégrité ZIP, Minecraft 26.3 et Fabric Loader 0.19.5,
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni module
de test. Les 3 303 fichiers d'assets, données et resourcepacks sont identiques
à beta.234, traductions incluses. Story reste dans le JAR de production et les
classes des bancs client en sont absentes. Reçu :
`build/pause235-mrpack-receipt.json`.
- JAR : `aad94732c749161d775ee634bdd85de70de511ef875c6c00fa167d230d0d600b`.
- MRpack : `5f1bd18aa8a4145b7e708da0abfff22068365847dddbc95ae2b490b88bbb305e`.
Aucune publication distante, modification du canal packwiz, synchronisation
Prism ou ouverture d'un monde personnel effectuée.
+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é.
+80
View File
@@ -0,0 +1,80 @@
# STORY-234 — recherche et filtres
Branche `codex/story-beta234`, base beta.233, commit `9c096b2`, Minecraft 26.3.
## Contrat
Story apparaît sous Progression dans Aventure, avant et après Atlas. Il reprend
le cadre de Discovery : titre, séparateurs et Terminé / Done. La recherche et
le filtre Toutes / Découvertes / Inconnues remplacent les boutons de catégories.
La liste réserve 100 histoires numérotées et inconnues, toutes désactivées.
Toutes et Inconnues montrent les entrées correspondant à la recherche, dans
l'ordre numérique ; Découvertes indique qu'aucune histoire n'est découverte.
Une recherche sans résultat dispose d'un autre message. La recherche porte sur
le numéro et le libellé visible, ignore la casse et les espaces extérieurs.
Recherche et filtre se combinent et ramènent le défilement en haut. La saisie
garde son focus et son curseur ; redimensionner garde recherche, filtre et
défilement. Terminé et Échap reviennent directement au menu Pause d'origine.
Aventure comprend maintenant Discovery, Progression et Story. Les trois groupes
de Pause conservent leur ordre avec ou sans carte. Aux petites hauteurs, les
boutons et espaces se compactent pour éviter les chevauchements.
[Guide de rédaction](story/README.md) et [modèle Markdown](story/TEMPLATE.md) :
un fichier `001.md` à `100.md`, avec numéro, titre, texte et idée de découverte
facultative. Aucun récit n'est inventé, chargé ou révélé dans ce lot. Leur
intégration et leurs déblocages seront des tickets suivants. Pas de nouveau
paquet réseau, dépendance, sauvegarde ou changement de génération.
## Vérifications
```sh
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Build final réussi en 2 min 34 s : quatre GameTests requis et contrôles purs du
dépôt, dont les contrôles communautaires fichier, photos, demandes et recherche.
Log : `build/story234-check-build-pack-final.log`. Le banc menus utilise la
sélection Atlas existante ; le conflit du générateur sur le chemin par défaut,
documenté en beta.232, n'est pas modifié par ce lot.
Parcours client final réussi en 30 s (`STORY234_PASS`), sur un monde plat neuf
de développement en Survie sans commandes. Vulkan confirmé par le journal :
MoltenVK 1.4.2 sur macOS, Apple M1. Log : `build/story234-client-vulkan-final.log`.
- FR/EN, GUI 2/3/4, fenêtre 1280 × 960, avec et sans Atlas : six actions
Sanctuary dans l'ordre visuel et clavier, trois groupes sans chevauchement,
groupe Discovery/Progression/Story centré verticalement.
- 100 numéros consécutifs sans doublon, entrées désactivées, recherche du numéro
100, espaces extérieurs et recherche en majuscules dans le libellé localisé.
Recherche et filtre combinés, deux états vides distincts.
- Saisie par événements natifs, focus et curseur conservés, molette et accès à
la dernière entrée ; défilement remis en haut après recherche ou filtre.
Redimensionnement réel à 1344 × 1008 : recherche, filtre et défilement conservés.
- Terminé et Échap reviennent à la même Pause. La page reste une pause solo ;
le record de progression serveur est identique avant et après les consultations.
21 captures dans `build/story234-screenshots/`. Captures inspectées : Story FR/2
et EN/4, Pause sans Atlas FR/4 et avec Atlas EN/3, états Découvertes et recherche
vides, recherche de l'histoire 100. L'indication de recherche reste visible
lorsque le champ vide a le focus. Aucun essai OpenGL ou Windows n'est revendiqué.
## Livraison locale
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.234.
Export [Sanctuary-beta.234.mrpack](../build/Sanctuary-beta.234.mrpack),
12 775 502 octets : intégrité ZIP, Minecraft 26.3, Fabric Loader 0.19.5,
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni
module de test. Le JAR de production inclut Story et aucune classe de son banc
client. Assets et données identiques à beta.233, hors les neuf traductions
ajoutées dans chacune des langues FR/EN ; les libellés existants sont conservés.
Reçu : `build/story234-mrpack-receipt.json`.
- JAR : `25dc161468679379e642f9746761905045d1135b6a300e3bc3f58f37a4c8f9a8`.
- MRpack : `0b16514e4dec16d14061fd25c7cb42d53fdb28c73f06a1c6361d16d059977c69`.
Les trois commits couvrent l'interface, les tests/documentation et la livraison.
Aucune publication distante, modification de canal packwiz, synchronisation
Prism ou ouverture de monde personnel effectuée.
+19
View File
@@ -0,0 +1,19 @@
# Écrire les histoires de Sanctuary
Story accueille des textes personnels sur Sanctuary, son développement, Steve,
Minecraft et ce que signifie habiter un jeu. Les numéros de 1 à 100 sont des
repères durables ; ils n'imposent ni un ordre de lecture ni une chronologie.
Copier [le modèle](TEMPLATE.md) dans un fichier nommé `001.md` à `100.md`, puis
remplacer le titre et écrire librement dans la section Texte. Les paragraphes,
les hésitations et la voix de l'auteur peuvent être conservés tels quels.
La section Idée de découverte est facultative et peut rester vide.
Transmettre les fichiers au fil des séances pour préparer les prochains tickets.
Ne pas renuméroter une histoire déjà intégrée. La traduction éventuelle des récits
se décide avec l'auteur ; les interfaces du jeu disposent de leurs libellés FR/EN.
En beta.234, les 100 histoires sont encore inconnues et désactivées. Ces fichiers
sont des sources de rédaction : le mod ne les charge pas automatiquement.
L'intégration des premiers textes et leurs conditions de découverte restent à
implémenter, avec autorité serveur si elles dépendent de la partie.
+12
View File
@@ -0,0 +1,12 @@
# Titre de l'histoire
Numéro : 001
## Texte
Écrire ici le texte, avec autant de paragraphes que nécessaire.
## Idée de découverte (facultative)
Décrire éventuellement un geste, une rencontre, un lieu ou une autre idée qui
pourrait révéler cette histoire. Cette note est une intention de conception.
+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.238
pack_version=beta.238
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}"
}
}
+40 -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 = [forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', 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,18 @@ 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 aptitudes236Client = providers.gradleProperty('sanctuaryAptitudes236ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('aptitudes236Client', aptitudes236Client)
if (aptitudes236Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Aptitudes236ClientChecks') }
def story234Client = providers.gradleProperty('sanctuaryStory234ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryStory234ClientTests', story234Client)
if (story234Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Story234ClientChecks') }
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 +570,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 +1366,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]
}
@@ -0,0 +1,105 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.recipes.RecipeService;
import fr.koka.sanctuary.food.FoodKnowledgeService;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
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.ClientGameTestContext;
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 static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native Progression widgets and real server requests in a disposable Vulkan world. */
public final class Aptitudes236ClientChecks implements FabricClientGameTest {
private static final Map<String,String> TITLES=Map.ofEntries(
Map.entry("names","sanctuary.progression.names"),Map.entry("atlas","sanctuary.progression.atlas"),
Map.entry("sorting","sanctuary.progression.sorting"),Map.entry("swimming","sanctuary.progression.swimming"),
Map.entry("resting","sanctuary.progression.resting"),Map.entry("active_bond","sanctuary.companion.link"),
Map.entry("familiar_study","sanctuary.companion.study"),Map.entry("catalogue","sanctuary.recipes.catalogue"),
Map.entry("zoom","sanctuary.zoom.title"),Map.entry("enchanting","sanctuary.progression.enchanting"),Map.entry("alchemy","sanctuary.progression.alchemy"),Map.entry("smithing","sanctuary.progression.smithing"));
@Override public void runTest(ClientGameTestContext c) {
var intro=new AtomicBoolean(true);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("Forgerie238 "+UUID.randomUUID());s.setSeed("238");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.setExperienceLevels(100);ProgressionService.send(p,"");});
c.waitFor(m->m.player.experienceLevel==100,200);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));check(!CraftAptitudes.learned(m.player,"enchanting") && !CraftAptitudes.learned(m.player,"alchemy") && !CraftAptitudes.learned(m.player,"smithing"),"New workstations start locked");});
for(String craft:List.of("enchanting","alchemy","smithing")) {
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get(craft));check(key(button).equals("sanctuary.progression.unlock_cost")&&button.active,"Locked craft offers its normal purchase button");button.onPress(enter());});
c.waitFor(m->CraftAptitudes.learned(m.player,craft) && key(afterTitle(m.gui.screen(),TITLES.get(craft))).equals("sanctuary.progression.aptitude_acquired"),200);
}
c.runOnClient(m->check(m.player.experienceLevel==88,"Three UI purchases cost twelve levels"));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(100);
for(String target:ProgressRecord.APTITUDES)check(ProgressionService.purchase(p,target,ProgressEffects.get(p).revision()),"Server acquires "+target);
check(CompanionService.buy(p),"Active Bond bought");check(CompanionStudy.buy(p),"Familiar Study bought");
check(RecipeService.buy(p),"Catalogue bought");check(ZoomService.buy(p),"Zoom bought");check(FoodKnowledgeService.buy(p),"Food Knowledge bought");
CompanionService.send(p,"");ProgressionService.send(p,"");
});
c.runOnClient(m->m.gui.setScreen(null));
c.waitFor(m->ProgressionClient.progress.names() && CompanionClient.studied && ZoomClient.learned() && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned() && FoodKnowledgeClient.ready(),300);
c.waitTicks(30);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(AptitudeService.PREFERENCES,2044);ProgressionService.send(p,"");check(p.getAttached(AptitudeService.PREFERENCES)==0,"Legacy preferences are normalized without losing purchases");});
c.runOnClient(m->{m.player.setAttached(AptitudeService.PREFERENCES,2044);for(String target:AptitudePreferences.PERMANENT)check(AptitudeClient.enabled(target),"Client ignores legacy disabled bits for "+target);});
String purchaseProof=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
int xp=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel);
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 ProgressionScreen(new PauseScreen(true)));});
c.waitTicks(3);
c.runOnClient(m->{var screen=m.gui.screen();check(screen.isPauseScreen(),"Progression retains Pause semantics");layout(screen);var titles=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).map(Aptitudes236ClientChecks::key).toList();
int previous=-1;for(String theme:List.of("survival","inventory_crafting","familiars","exploration","crafts")){int index=titles.indexOf("sanctuary.progression.theme_"+theme);check(index>previous,"All five themes are present in order");previous=index;}
check(has(screen,"sanctuary.progression.aptitude_permanent"),"Food Knowledge is visibly permanent");
check(key(afterTitle(screen,"sanctuary.food_knowledge.title")).equals("sanctuary.food_knowledge.learned") && !afterTitle(screen,"sanctuary.food_knowledge.title").active,"Food Knowledge has no activation switch");
for(var entry:TITLES.entrySet()){var button=afterTitle(screen,entry.getValue());boolean toggle=AptitudePreferences.TOGGLEABLE.contains(entry.getKey());
check(key(button).equals(toggle?"sanctuary.progression.aptitude_enabled":"sanctuary.progression.aptitude_acquired") && button.active==toggle,"Only World Map and Mob Names have switches: "+entry.getKey());check(m.font.width(button.getMessage())+8<=button.getWidth(),"Aptitude label fits without clipping");}
});
shot(c,"top-"+language+"-"+scale);
for(String target:AptitudePreferences.TOGGLEABLE) {
c.runOnClient(m->{var screen=m.gui.screen();var button=afterTitle(screen,TITLES.get(target));reveal(screen,button);check(button.active,"Toggle is ready: "+target);button.onPress(enter());});
c.waitFor(m->!AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_disabled"),200);
check(server.computeOnServer(s->!AptitudeService.enabled(s.getPlayerList().getPlayers().getFirst(),target)),"UI choice is authoritative: "+target);
if(target.equals("atlas")) {
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),false);});shot(c,"pause-map-disabled-"+language+"-"+scale);
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(new PauseScreen(true))));
}
c.runOnClient(m->afterTitle(m.gui.screen(),TITLES.get(target)).onPress(enter()));
c.waitFor(m->AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_enabled"),200);
}
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),true);});shot(c,"pause-map-enabled-"+language+"-"+scale);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));reveal(m.gui.screen(),afterTitle(m.gui.screen(),TITLES.get("smithing")));});
shot(c,"crafts-"+language+"-"+scale);
c.runOnClient(m->{var screen=m.gui.screen();var area=aptitudes(screen);area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
c.runOnClient(m->{check(aptitudes(m.gui.screen()).scrollAmount()==100,"Resize retains aptitude scroll");layout(m.gui.screen());press(m.gui.screen(),"gui.done");check(m.gui.screen() instanceof PauseScreen,"Done returns to Pause");m.gui.setScreen(null);});
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.experienceLevel==xp && p.getAttached(ProgressionService.RECORD).equals(purchaseProof),"Twelve disable/reactivate pairs preserve XP and purchases");});
System.out.println("FORGERIE238_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, eleven permanent purchases, legacy preferences and merged Pause layout, three native UI purchases, preserved XP/purchases, scroll/resize and Done");
}finally{intro.set(false);}
}
private static void pauseLayout(Screen screen,boolean enabled) {Pause233ClientChecks.pause(screen,enabled);}
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
private static Button afterTitle(Screen screen,String title) {
var list=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
int index=java.util.stream.IntStream.range(0,list.size()).filter(i->key(list.get(i)).equals(title)).findFirst().orElseThrow(()->new AssertionError("Missing title "+title));
return list.subList(index+1,list.size()).stream().filter(Button.class::isInstance).map(Button.class::cast).findFirst().orElseThrow();
}
private static AbstractScrollArea aptitudes(Screen screen){return widgets(screen).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).max(Comparator.comparingInt(AbstractWidget::getX)).orElseThrow();}
private static void reveal(Screen screen,AbstractWidget widget){var area=aptitudes(screen);area.setScrollAmount(Math.clamp(area.scrollAmount()+widget.getBottom()-area.getBottom()+4,0,area.maxScrollAmount()));}
private static void layout(Screen screen){var area=aptitudes(screen);check(area.getX()>=0&&area.getRight()<=screen.width&&area.getBottom()<screen.height-26,"Aptitudes viewport stays inside native frame");for(var button:widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).toList())check(button.getX()>=0&&button.getRight()<=screen.width,"Buttons remain within the screen horizontally");}
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("forgerie238-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/forgerie238-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
}
@@ -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,149 @@
package fr.koka.sanctuary.client;
import com.google.gson.Gson;
import com.mojang.blaze3d.platform.InputConstants;
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.client.input.KeyEvent;
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();
// Wait and dispatch together so the periodic following request cannot take the slot between them.
c.waitFor(m->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")),200);
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, parallel Adventure/Community before Atlas, narrow Community/Adventure stack beside Atlas, Game directly below, centered navigation block, visual Tab order, 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"));}
}
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.story.title","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()==6,"Six Sanctuary navigation actions");
var options=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("menu.options")).findFirst().orElseThrow();
var worldOptions=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("options.worldOptions.button")).findFirst().orElseThrow();
var quit=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("menu.returnToMenu","menu.disconnect").contains(key(b))).findFirst().orElseThrow();
var community=widget(s,"pause_group_community");var adventure=widget(s,"pause_group_adventure");var game=widget(s,"pause_group_game");
check(nav.stream().map(CommunityPausePanels::key).toList().equals(unlocked
?List.of("sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title")
:List.of("sanctuary.progression.blocks","sanctuary.progression.identity","sanctuary.progression.progress","sanctuary.cycle.faction","sanctuary.story.title","sanctuary.arena.title")),"Navigation follows visual order in both layouts");
for(var button:nav)check(s.getFont().width(button.getMessage())+8<=button.getWidth(),"Navigation labels fit with native button padding");
check(nav.stream().allMatch(b->b.getBottom()<game.getY())&&game.getBottom()<options.getY()&&options.getBottom()<worldOptions.getY()&&worldOptions.getBottom()<quit.getY(),"Game follows below Sanctuary navigation");
check(options.getX()==nav.get(0).getX()&&worldOptions.getX()==options.getX()&&quit.getX()==options.getX()&&worldOptions.getWidth()==options.getWidth()&&quit.getWidth()==options.getWidth(),"Native Game actions share the navigation block width");
int bodyTop=s.height<280?46:50,bodyBottom=s.height-39;
check(Math.abs((Math.min(adventure.getY(),community.getY())+quit.getBottom())/2.-(bodyTop+bodyBottom)/2.)<=.5&&quit.getBottom()+8<=bodyBottom,"Complete block centered with clear space below Game");
// KeyEvent carries the physical key and the SDL shortcut key separately.
s.setFocused(nav.get(0));s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(1),"Tab follows the second visible action");
s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(2),"Tab follows the third visible action");
if(!unlocked){
check(adventure.getY()==community.getY()&&adventure.getX()==nav.get(0).getX()&&community.getX()==nav.get(1).getX()&&adventure.getBottom()<nav.get(0).getY(),"Both headings align above their columns");
for(int row=0;row<3;row++) {
var left=nav.get(2*row);var right=nav.get(2*row+1);
check(left.getY()==right.getY()&&left.getHeight()==right.getHeight()&&left.getRight()<right.getX(),"Two parallel columns without overlap");
if(row>0)check(nav.get(2*row-2).getBottom()<left.getY(),"Rows remain separated");
}
check(options.getRight()==nav.get(1).getRight(),"Game spans both columns before Atlas");
check(Math.abs((nav.get(0).getX()+nav.get(1).getRight())/2.-s.width/2.)<=.5,"Two-column block centered horizontally");
check(widget(s,"article").getX()==8&&widget(s,"article").getRight()<nav.get(0).getX()&&widget(s,"notice").getX()>nav.get(1).getRight(),"Gazette remains left and Notice Board right");
}
else {
check(community.getBottom()<nav.get(0).getY()&&nav.get(2).getBottom()<adventure.getY()&&adventure.getBottom()<nav.get(3).getY(),"Community then Adventure stacked above Game with Atlas");
for(int i=0;i<nav.size();i++) {
var button=nav.get(i);check(button.getX()==8&&button.getWidth()==options.getWidth()&&button.getWidth()<=132,"All actions use one narrow left column");
if(i>0)check(nav.get(i-1).getBottom()<button.getY(),"Stacked buttons remain separated");
}
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();check(nav.get(0).getRight()<map.getX()&&map.getRight()<widget(s,"article").getX()&&widget(s,"article").getX()==widget(s,"notice").getX(),"Narrow navigation left, map center and community feeds 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,93 @@
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.story.title","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()==9,"All nine 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()==(screen.height<280?46:50)&&notice.getY()==article.getY(),"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");
Pause233ClientChecks.pause(screen,unlocked);
});
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,72 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
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.ClientGameTestContext;
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.client.input.CharacterEvent;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.network.chat.contents.TranslatableContents;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Real native events on Story and Pause, using a fresh non-operator solo world. */
public final class Story234ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
var intro=new AtomicBoolean(true);
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("Story234 "+UUID.randomUUID());s.setSeed("234");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->s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
for(boolean unlocked:new boolean[]{false,true}) {
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
String before=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
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.waitTicks(3);
c.runOnClient(m->Pause233ClientChecks.pause(m.gui.screen(),unlocked));
shot(c,"pause-"+unlocked+"-"+language+"-"+scale);
c.runOnClient(m->{var pause=m.gui.screen();press(pause,"sanctuary.story.title");check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
shot(c,"story-"+language+"-"+scale);
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);for(int codepoint:"100".codePoints().toArray()){check(s.charTyped(new CharacterEvent(codepoint)),"Native typing handled");check(s.getFocused()==search&&search.isFocused(),"List refresh retains search focus");}check(search.getValue().equals("100")&&search.getCursorPosition()==3,"Continuous typing retains cursor");assertEntries(s,1);check(entries(s).getFirst().getMessage().getString().equals(StoryScreen.label("unknown_entry",100).getString()),"Search selects story 100");search.setValue(" 100 ");assertEntries(s,1);cycle(s).onPress(enter());check(has(s,"sanctuary.story.no_discoveries")&&!has(s,"sanctuary.story.no_results")&&entries(s).isEmpty(),"Discovered has a distinct empty state");cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("UNKNOWN"),"Third filter is Unknown");assertEntries(s,1);search.setValue("absent-story");check(has(s,"sanctuary.story.no_results")&&!has(s,"sanctuary.story.no_discoveries"),"Search empty state is distinct");search.setValue(language.equals("fr_fr")?"INCONNUE":"UNKNOWN");assertEntries(s,100);var area=area(s);check(s.mouseScrolled(area.getX()+5,area.getY()+5,0,-5)&&area.scrollAmount()>0,"Native wheel scrolls Story");area.setScrollAmount(area.maxScrollAmount());check(entries(s).getLast().getBottom()<=area.getBottom(),"Story 100 can be reached at the end");area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
c.runOnClient(m->{var s=m.gui.screen();check(edit(s).getValue().equals(language.equals("fr_fr")?"INCONNUE":"UNKNOWN")&&cycle(s).getValue().toString().equals("UNKNOWN"),"Resize retains query and filter");check(area(s).scrollAmount()==100,"Resize retains scroll position");layout(s);edit(s).setValue("100");check(area(s).scrollAmount()==0,"Search resets scroll");edit(s).setValue("");area(s).setScrollAmount(100);cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("ALL")&&area(s).scrollAmount()==0,"Filter resets scroll and cycles back to All");assertEntries(s,100);});
if(!unlocked&&language.equals("fr_fr")&&scale==2) {
c.runOnClient(m->cycle(m.gui.screen()).onPress(enter()));shot(c,"empty-discovered");
c.runOnClient(m->{cycle(m.gui.screen()).onPress(enter());edit(m.gui.screen()).setValue("absent-story");});shot(c,"empty-search");
c.runOnClient(m->edit(m.gui.screen()).setValue("100"));shot(c,"search-100");
}
c.runOnClient(m->{var story=(StoryScreen)m.gui.screen();var pause=story.parent;story.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE,0,0));check(m.gui.screen()==pause,"Escape returns directly to original Pause");press(pause,"sanctuary.story.title");press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"Done returns directly to original Pause");m.gui.setScreen(null);});
c.waitTicks(2);
}
server.runOnServer(s->check(before.equals(s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD)),"Reading and searching Story leave authoritative progression unchanged"));
}
System.out.println("STORY234_PASS: Vulkan FR/EN GUI 2/3/4, Atlas gating, 100 disabled entries, live search, three filters, distinct empty states, typing focus, wheel, resize, Escape/Done, unchanged progression");
c.runOnClient(m->m.gui.setScreen(null));
}finally{intro.set(false);}
}
private static List<Button> entries(Screen s){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("sanctuary.story.unknown_entry")).toList();}
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
private static CycleButton<?> cycle(Screen s){return widgets(s).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();}
private static AbstractScrollArea area(Screen s){return widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();}
private static void assertEntries(Screen s,int expected){var rows=entries(s);check(rows.size()==expected,"Expected "+expected+" entries, got "+rows.size());check(rows.stream().allMatch(b->!b.active),"Unknown entries stay disabled");if(expected==100)for(int i=0;i<100;i++)check(rows.get(i).getMessage().getString().equals(StoryScreen.label("unknown_entry",i+1).getString()),"Numbers stay ordered without gaps or duplicates");}
private static void layout(Screen s){var search=edit(s);var filter=cycle(s);var list=area(s);check(search.getBottom()<filter.getY()&&filter.getBottom()<list.getY(),"Search, filter and list do not overlap");for(var child:s.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Top-level widget stays inside the screen");var done=widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("gui.done")).findFirst().orElseThrow();check(list.getBottom()<done.getY(),"List leaves room for native footer");}
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("story234-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/story234-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
}
@@ -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,129 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.recipes.*;
import fr.koka.sanctuary.inventory.*;
import fr.koka.sanctuary.atlas.AtlasService;
import java.util.UUID;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.storage.*;
import net.minecraft.util.ProblemReporter;
import net.minecraft.network.chat.Component;
/** Behavioral checks on real server players, native menus and vanilla persistence. */
public final class Aptitudes236GameTests {
@GameTest public void preferencesPreservePurchasesXpDeathAndCycle(GameTestHelper h) {
var record=ProgressRecord.INITIAL;
for(String skill:ProgressRecord.SKILLS)while(record.rank(skill)<ProgressRecord.cap(skill))record=record.buy(skill,record.cost(skill,ProgressionConfig.DEFAULT));
for(String aptitude:ProgressRecord.APTITUDES)record=record.buy(aptitude,4);
var p=player(h,record);p.setExperienceLevels(20);
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
p.setAttached(CompanionStudy.RECEIPT,"{\"schema\":1,\"paid\":4,\"at\":1}");
p.setAttached(ZoomService.LEARNED,true);
h.assertTrue(RecipeService.buy(p),"Catalogue acquired through its normal server purchase");
h.assertTrue(CraftAptitudes.buy(p,"enchanting",record.revision()) && CraftAptitudes.buy(p,"alchemy",record.revision()),"New craft purchases accepted");
String proof=p.getAttachedOrThrow(ProgressionService.RECORD),recipes=p.getAttached(RecipeService.RECORD),bond=p.getAttached(CompanionService.RECORD);
int xp=p.experienceLevel;
h.assertTrue(!p.hasAttached(AptitudeService.PREFERENCES),"Old players need no preference migration");
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,false),"Owned aptitude can be disabled: "+target);
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && p.experienceLevel==xp,"Both display choices are independent and free");
h.assertTrue(proof.equals(p.getAttached(ProgressionService.RECORD)) && recipes.equals(p.getAttached(RecipeService.RECORD)) && bond.equals(p.getAttached(CompanionService.RECORD)),"Purchase proofs remain byte-for-byte unchanged");
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(AptitudeService.preferences(loaded).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Choices and craft purchases reload from vanilla player NBT");
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
h.assertTrue(AptitudeService.preferences(respawn).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(respawn,"alchemy"),"Choices and new purchases survive ordinary death");
bind(p,record.nextCycle());
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && CraftAptitudes.learned(p,"enchanting"),"Skill reset preserves separate preferences and craft purchases");
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,true),"Reactivation needs no new purchase: "+target);
h.assertTrue(p.experienceLevel==xp && AptitudeService.preferences(p).equals(AptitudePreferences.DEFAULT),"Reactivation costs no XP");h.succeed();
}
@GameTest public void unknownUnownedPermanentAndStaleRequestsRefused(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL.buy("names",4));p.setExperienceLevels(10);
for(String target:new String[]{"swimming","enchanting","alchemy","food_knowledge","unknown","vitality"})
h.assertTrue(!toggle(p,target,false),"Cannot toggle unowned, permanent or non-aptitude target: "+target);
h.assertTrue(toggle(p,"names",false),"Owned name reveal disables");
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("names",true,0)),"Stale mask cannot overwrite a newer choice");
h.assertTrue(!CraftAptitudes.buy(p,"enchanting",99) && !CraftAptitudes.buy(p,"unknown",1),"Stale and unknown purchases refused");
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==3,"Insufficient XP never spent");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==0 && !CraftAptitudes.buy(p,"alchemy",1),"Four levels buy once");
h.assertTrue(!AptitudeService.enabled(p,"names"),"Refusal preserves previous choice");h.succeed();
}
@GameTest public void legacyPreferencesRestorePermanentPurchases(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));p.setExperienceLevels(12);
h.assertTrue(CraftAptitudes.buy(p,"enchanting",2) && CraftAptitudes.buy(p,"alchemy",2),"Craft purchases exist before legacy migration");
p.setAttached(AptitudeService.PREFERENCES,AptitudePreferences.MAX_MASK);
String proof=p.getAttached(ProgressionService.RECORD);int xp=p.experienceLevel;
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==2047,"Historical codec still reloads all eleven bits");
h.assertTrue(AptitudeService.preferences(loaded).disabledMask()==3 && !AptitudeService.enabled(loaded,"names") && !AptitudeService.enabled(loaded,"atlas"),"Legacy display choices retain their original bit positions");
for(String target:AptitudePreferences.PERMANENT) {
h.assertTrue(AptitudeService.enabled(loaded,target),"Legacy permanent preference has no effect: "+target);
h.assertTrue(!toggle(p,target,false),"Permanent aptitude cannot be disabled: "+target);
}
AptitudeService.send(loaded);
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==3 && loaded.experienceLevel==xp && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Sync normalizes only preferences, preserving XP and purchase proofs");
h.assertTrue(CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Migration retains both craft purchases");h.succeed();
}
@GameTest public void permanentServerEffectsIgnoreLegacyDisabledBits(GameTestHelper h) {
var r=ProgressRecord.INITIAL.buy("hunger",1).buy("swimming",4).buy("resting",4).buy("sorting",4).buy("atlas",4);
var p=player(h,r);p.getFoodData().setFoodLevel(8);p.setAttached(AptitudeService.PREFERENCES,2044);
var at=p.blockPosition();for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
p.baseTick();p.baseTick();p.setSprinting(true);p.setSwimming(true);
h.assertTrue(p.isSwimming() && !toggle(p,"swimming",false) && p.isSwimming() && ProgressEffects.canSwim(p),"Swimming stays usable with an old disabled bit and a rejected request");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.AIR.defaultBlockState());
p.baseTick();p.baseTick();p.setOnGround(true);
h.assertTrue(LyingService.request(p,new LyingService.Request(2,r.revision())),"Purchased Rest ignores its legacy disabled bit");
h.assertTrue(!toggle(p,"resting",false) && LyingService.posture(p)==2,"Refused disable request never stops Rest");
h.assertTrue(InventorySortService.sort(p,new InventorySortPayloads.Sort(1,p.containerMenu.containerId,p.containerMenu.getStateId(),r.revision(),InventorySortPayloads.HOTBAR)),"Purchased inventory sorting ignores its legacy disabled bit");
h.assertTrue(AtlasService.canRead(p,false) && toggle(p,"atlas",false) && !AtlasService.canRead(p,false),"Personal World Map access follows the display choice");
h.assertTrue(toggle(p,"atlas",true) && AtlasService.canRead(p,false),"World Map reactivates with its existing purchase");
LyingService.request(p,new LyingService.Request(0,r.revision()));
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
h.assertTrue(!toggle(p,"active_bond",false) && CompanionService.activate(p,-1).equals("no_familiar"),"Active Bond reaches the normal familiar requirement despite its legacy disabled bit");h.succeed();
}
@GameTest public void enchantingNativeMenuRequiresPermanentPurchase(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.ENCHANTING_TABLE.defaultBlockState());
var provider=new SimpleMenuProvider((id,inventory,player)->new EnchantmentMenu(id,inventory,ContainerLevelAccess.create(h.getLevel(),at)),Component.literal("Enchanting"));
h.assertTrue(p.openMenu(provider).isEmpty() && p.containerMenu==p.inventoryMenu,"Unlearned player cannot open a native enchanting menu");
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"enchanting",0) && p.experienceLevel==36,"Unlock charges exactly four levels");
h.assertTrue(p.openMenu(provider).isPresent(),"Native menu opens after purchase");var menu=(EnchantmentMenu)p.containerMenu;
menu.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));menu.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI,3));
h.assertTrue(menu.clickMenuButton(p,0) && !menu.getSlot(0).getItem().get(DataComponents.ENCHANTMENTS).isEmpty(),"Real vanilla enchanting remains functional");
int xp=p.experienceLevel;
h.assertTrue(!toggle(p,"enchanting",false) && p.containerMenu==menu && menu.stillValid(p) && p.experienceLevel==xp,"Purchased enchanting cannot be disabled or closed by a toggle request");
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Enchanting remains available on later visits");p.closeContainer();h.succeed();
}
@GameTest public void alchemyNativeMenuRetainsSharedInventory(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.BREWING_STAND.defaultBlockState());
var stand=(net.minecraft.world.level.block.entity.BrewingStandBlockEntity)h.getLevel().getBlockEntity(at);
h.assertTrue(p.openMenu(stand).isEmpty(),"Unlearned player cannot open brewing stand");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",0) && p.openMenu(stand).isPresent(),"Alchemy unlock opens the native stand");
stand.setItem(3,new ItemStack(Items.NETHER_WART));stand.setItem(4,new ItemStack(Items.BLAZE_POWDER));
var menu=p.containerMenu;h.assertTrue(!toggle(p,"alchemy",false) && p.containerMenu==menu && menu.stillValid(p),"Alchemy cannot be disabled or closed by a toggle request");
h.assertTrue(stand.getItem(3).is(Items.NETHER_WART) && stand.getItem(4).is(Items.BLAZE_POWDER),"Shared ingredients are never removed");
p.closeContainer();h.assertTrue(p.openMenu(stand).isPresent(),"Purchased Alchemy stays available on later visits");
p.closeContainer();var vanilla=Progression003GameTests.player(h);
h.assertTrue(CraftAptitudes.allowed(vanilla,"enchanting") && CraftAptitudes.allowed(vanilla,"alchemy"),"Inactive vanilla players retain native access");h.succeed();
}
private static boolean toggle(ServerPlayer p,String target,boolean enabled) {return AptitudeService.set(p,new AptitudeService.Set(target,enabled,AptitudeService.preferences(p).disabledMask()));}
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"apt"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
catch(Exception e){throw new AssertionError(e);}
bind(p,record);return p;
}
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
}
@@ -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();
}
}

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