Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309d0ba6f3 | ||
|
|
5dee98106e | ||
|
|
d6f3258442 | ||
|
|
1c8e6437d5 | ||
|
|
00d175cb98 | ||
|
|
a7bdfe6998 | ||
|
|
f9c2ce8835 | ||
|
|
3d56d7de7f | ||
|
|
f2ac23b4f4 | ||
|
|
9c096b2563 | ||
|
|
bc27606b3e | ||
|
|
8531bb3476 | ||
|
|
8e9b8fbd62 | ||
|
|
308c0dc2ff | ||
|
|
50aaa5c135 | ||
|
|
cf5699cfe1 | ||
|
|
d9979100a9 | ||
|
|
5f3d110773 | ||
|
|
a2201cc422 | ||
|
|
42e42fea2a | ||
|
|
55df522b1e | ||
|
|
b77d71bd0d | ||
|
|
70a34df2d5 | ||
|
|
5fb1407705 | ||
|
|
e2b9ee7aaf | ||
|
|
1388d8a443 | ||
|
|
4554ea1882 | ||
|
|
9063e16e7c | ||
|
|
2d656d9525 | ||
|
|
a3dd17c37a | ||
|
|
58aa4c2ae1 | ||
|
|
988a6c8f51 | ||
|
|
3adb08eaa6 | ||
|
|
f8456df256 | ||
|
|
0eb4517961 | ||
|
|
03aab49996 | ||
|
|
b82ab378f0 | ||
|
|
da838b9e8e | ||
|
|
3941bd4711 | ||
|
|
7375046488 | ||
|
|
2ee53e1065 | ||
|
|
efb1097b65 | ||
|
|
d021341d3e | ||
|
|
bb10c24ada | ||
|
|
6f780547bb | ||
|
|
bf5df3885b | ||
|
|
2463db72f7 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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é.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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é.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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é.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
-4
@@ -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}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
+105
@@ -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);}}
|
||||
}
|
||||
+15
-13
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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");
|
||||
|
||||
+149
@@ -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);}}
|
||||
}
|
||||
+93
@@ -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)&¬ice.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()&¬ice.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);}
|
||||
}
|
||||
+72
@@ -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);}}
|
||||
}
|
||||
+45
@@ -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);}
|
||||
}
|
||||
+6
-4
@@ -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);}
|
||||
|
||||
+129
@@ -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
Reference in New Issue
Block a user