Compare commits

..
Author SHA1 Message Date
koka 454d9a5c08 Add fractured northern relief and giant taiga for beta.203
Build Sanctuary / build (push) Canceled after 0s
2026-10-04 13:13:17 +02:00
koka ba1b228e76 Refine northern ecology and snowy igloo placement for beta.202 2026-10-04 12:42:16 +02:00
koka 1d60a3eedc feat: compact northern glacier and use vanilla ice spikes in beta.201 2026-10-04 12:05:38 +02:00
koka 16d81d892f feat: deliver beta.200 North expansion and igloo village 2026-10-04 05:52:20 +02:00
koka 7497d9182d beta.199: connect runtime expansions and synchronize the stellar week 2026-10-01 21:38:44 +02:00
koka 6b97d57bde Fix seed-dependent central gallery creation in beta.198 2026-10-01 20:02:28 +02:00
koka 7c14696020 beta.197: repair island arrival, dungeon details and seed-dependent gallery creation 2026-10-01 19:37:37 +02:00
koka 39ff7e95d5 beta.196: share the current island lab in three sizes and profile loading 2026-10-01 18:40:01 +02:00
koka 918143aaa6 beta.195: hide legacy lab presets and open the seed zero visit 2026-10-01 18:02:21 +02:00
koka 9da3f7f13b beta.194: remove Friends from the title menu and pause terrain work 2026-10-01 01:42:49 +02:00
koka 7179b5d4cb beta.193: protect worldgen water and add native multiseed photo comparisons 2026-10-01 01:29:17 +02:00
koka 78f047d3dc beta.192: sculpt local summit crowns and rocky mountain faces 2026-09-30 22:34:43 +02:00
koka c7f32c9416 beta.191: restore pre-terrace relief and original surface geology 2026-09-30 21:47:10 +02:00
koka 97ea94e482 beta.190: restore organic relief, widen spillways and stock ruin chests 2026-09-30 21:05:30 +02:00
koka 51e7e2590e beta.189: fit refuges to terrain and finish shores, terraces and cave ruins 2026-09-30 20:31:06 +02:00
koka 658aaf8732 beta.188: add living lake beds, sulfur details and enchanted iron loot 2026-09-30 09:04:25 +02:00
koka c53b39e71c Integrate beta.187 natural sulfur caves, surface lakes and oriented anchors 2026-09-30 08:41:38 +02:00
koka f40382ad04 Build beta.186 ornamental rosettes and eight terrain-bound anchors 2026-09-30 08:09:11 +02:00
koka a067c7e66b Build beta.185 fine stem and integrated palace roof portal 2026-09-30 07:33:49 +02:00
koka 6cfa4c07bf Build beta.184 natural sky palace and fill retained terrain cavities 2026-09-30 03:46:00 +02:00
koka 1ca01a47b7 Confirm beta.183 solo entry and spectator visit 2026-09-30 03:12:37 +02:00
koka a46946bd4c Add beta.183 island discoveries and adaptive ISS atlas 2026-09-30 03:10:10 +02:00
koka 65e7127b1d Add beta.182 natural lower gallery and eight-axis glass rosette 2026-09-30 02:23:35 +02:00
274 changed files with 19997 additions and 158 deletions
+170
View File
@@ -1,5 +1,175 @@
# Sanctuary
## beta.203 — fractures et taïga géante au Nord
[Dernière passe du Nord](docs/north-fracture-beta203.md) : variations de relief
et ouvertures par bruit 3D, lisières de podzol irrégulières, épicéas pouvant
atteindre 64 blocs. La neige ajoutée après végétation est retirée ; les sommets
mêlent roche, neige et plusieurs glaces. Bassins protégés, cinq igloos conservés,
aucun nouveau village. Profils neufs seulement, Nord 1024.
## beta.202 — taïga et neige étagée au Nord
[Écologie nordique](docs/north-ecology-beta202.md) : relief et lacs 201 conservés,
vallées ouvertes et taïga sur herbe/podzol, grands épicéas et pics de glace natifs.
La neige varie avec l'altitude et le climat ; la glace des grottes reste derrière
une enveloppe rocheuse. Le village cherche une plaine entièrement enneigée.
Profils neufs uniquement, expansion de 1024 blocs.
## beta.201 — Nord compact et pics de glace vanilla
[Glacier nordique](docs/north-glacier-beta201.md) : diamètre ramené à 1024,
glace visible, deux lacs, bosquets d'épicéas et pics natifs `ice_spike` / `ice_patch`.
Le village de cinq igloos et son laboratoire sont conservés et rapprochés du lac.
Profils neufs uniquement ; les paysages froids des diagonales restent à concevoir.
## beta.200 — premier Nord glacial
[Expansion nordique](docs/north-expansion-beta200.md) : territoire de 2048 blocs,
crêtes enneigées, taïga, cavernes de glace, lacs gelés et village de cinq igloos,
dont un laboratoire souterrain. Placement dans la direction de l’ancre ; arrivée
et relais préparés, puis génération pendant l’exploration. Nouveaux mondes uniquement.
Export local : [MRpack de test](build/Sanctuary-Test-beta.200.mrpack).
Compilation, contrôles ciblés et activation/reprise du Nord vérifiés ; la suite
historique complète reste non validée, avec le détail dans le ticket.
## beta.199 — relais d’expansion en cours de partie
[Affinage du moteur existant](docs/runtime-expansions-beta199.md) : recherche
automatique proche sur 360°, régions minces, relais de continuation, reprise
après fermeture et annonce SGA à la fin. Nouveaux mondes de laboratoire
Small/Medium/Large ; étoiles synchronisées sur sept journées Minecraft.
## beta.198 — correction du plantage de création Large
[Admission de la galerie centrale](docs/gallery-generation-beta198.md) :
correction du défaut reproduit avec la graine Windows fournie, en conservant
le portail au centre et les anciens profils. Nouveaux mondes uniquement.
## beta.197 — arrivée et détails du laboratoire
[Corrections du laboratoire](docs/worldgen-fixes-beta197.md) : départ sur
l’île, cascades ouvertes vers l’extérieur, sol minier sans diagonales répétées
et toit de cabane continu. Nouveaux profils Small/Medium/Large ; enquête séparée
sur l’arrêt signalé chez un ami à la création Large.
## beta.196 — laboratoire à partager
[Pack de test Small, Medium et Large](docs/shared-lab-beta196.md) : une entrée
Sanctuary, taille personnalisable, dernier terrain du labo et atlas adapté.
Diagnostic du chargement et enregistrement JFR optionnel ; anciens mondes conservés.
## beta.195 — choix de génération simplifiés
[Sanctuary et les types vanilla](docs/worldgen-options-beta195.md) restent dans
la création de monde ; les variantes de labo sont masquées, leurs sauvegardes
restent lisibles. Nouvelle visite demandée sur le profil beta.193, graine 0,
avec le menu principal sans Amis. Le terrain est conservé pour cette comparaison.
## beta.194 — menu principal sans Amis
[Reprise des interfaces](docs/main-menu-beta194.md) : Solo et Multijoueur sont suivis
directement d’Options et Quitter, sans bouton Amis ni ligne vide. Le
[fil rouge](docs/storyquest-fil-rouge.md) reprend les interfaces et le Storyquest ;
les retouches de génération et les séries de captures sont suspendues.
## beta.193 — raccordements et comparaison visuelle
[Laboratoire sur plusieurs graines](docs/worldgen-continuity-beta193.md) : réservations
communes pour les bassins, ancres aériennes posées sur le terrain réel et
entailles locales du massif. Le nouveau profil `coherent` ajoute des contrôles
de collision entre aménagements et des visites photo Vulkan reproductibles.
Les anciens profils et sauvegardes restent séparés.
## beta.192 — couronnes et faces du massif
[Variante de relief érodé](docs/eroded-massif-beta192.md) : sommets localement
plus plats, faces rocheuses plus raides et entailles entre les reliefs.
Profil neuf `eroded`, graine 42 ; couverture végétale selon la pente réelle.
Les cavités profondes et les aménagements existants restent conservés.
## beta.191 — relief d’avant les plateaux
[Retour à la base beta.188](docs/restore-relief-beta191.md) : relief et géologie
antérieurs aux plateaux, passes de roche 189/190 retirées. Les corrections
indépendantes des berges, ancres, ruines et du portail restent présentes.
Profil neuf `original`, graine 42 ; génération, réouverture, 265 GameTests et
assemblages validés.
## beta.190 — retour au relief naturel
[Affinage de Sanctuary Island](docs/natural-refinement-beta190.md) : fin des
terrasses systématiques, épaules locales et affleurements sans cases de quatre
blocs. Déversoir proche de cinq blocs de large, coffres de fer enchanté dans les
quatre ruines. Profil `refined`, nouveau solo graine 42, vue 32 chunks.
Les reliefs très étagés passent dans la réserve des expansions futures.
## beta.189 — berges, reliefs et portail inférieur
[Finition de la génération](docs/terrain-finish-beta189.md) : refuges de surface
adaptés à la roche, sédiments selon profondeur et pente, berges sableuses,
plantations d’arbres protégées, reliefs étagés et affleurements. Portail inférieur
éclairé, cascade locale et quatre ruines sans excavation. Profil `finish`, nouvelle
sauvegarde, graine 42. 265 GameTests, génération, réouverture et assemblages réussis.
## beta.188 — fonds d’étangs et soufre vivant
[Laboratoire de finition](docs/living-details-beta188.md) : refuges adaptés
au milieu et à leur couleur, fonds mêlant argile, boue, sable et gravier,
végétation aquatique, pics et geysers natifs. Wagonnets avec une ou deux
pièces en fer enchanté. Profil `details`, monde neuf, graine 42.
265 GameTests, contrôles natifs et assemblages réussis ; solo Vulkan ouvert
à 32 chunks. Les anciennes sauvegardes restent inchangées.
## beta.187 — cavités de soufre et bassins de surface
[Nouveau laboratoire](docs/natural-caves-beta187.md) : soufre intégré aux
cavités existantes, trois bassins ouverts au ciel et huit ancres orientées
vers leur expansion, avec approche depuis le centre. Profil `natural`,
graine 42, nouvelle sauvegarde. Génération et réouverture vérifiées,
265 GameTests et assemblages réussis ; solo Vulkan ouvert à 32 chunks.
## beta.186 — rosaces et huit refuges d'ancres
[Nouveau laboratoire](docs/hidden-anchors-beta186.md) : balcon supérieur sans
barrières, liserés incrustés dans les deux dômes et huit ancres adaptées au terrain
de leur secteur. Dépôts distincts reliés aux gemmes du Bugrock. Profil `anchors`,
graine 42, nouvelle sauvegarde. Génération et réouverture vérifiées, 265 GameTests
et assemblages réussis ; solo Vulkan ouvert à 32 chunks.
## beta.185 — tige fine et portail de toiture
[Variante de laboratoire](docs/fine-stem-beta185.md) : ligne de calcite d’un bloc,
sans escalier ni paliers, fleur conservée, portail horizontal incrusté au sommet
et jour fermé sous les vitres du palais. Profil `stem`, monde neuf, graine 42.
265 GameTests, contrôles natifs et assemblages réussis ; solo Vulkan ouvert
à 32 chunks, départ dans le palais supérieur.
## beta.184 — ascension naturelle
[Nouveau laboratoire](docs/natural-ascent-beta184.md) : rosace et Bugrock à Y=640,
tige parcourable, palais vitré vers Y=1280 avec atlas au sol et portail horizontal.
La couronne se découvre en montant. Les bassins supérieurs dessinés sont remplacés
par le remplissage de creux existants. Profil `ascent`, nouveau monde uniquement.
265 GameTests, contrôles natifs et assemblages réussis ; solo Vulkan ouvert à
32 chunks, départ sur la rosace.
## beta.183 — lieux et ressources de l’île
[Nouveau laboratoire](docs/island-discovery-beta183.md) : portail horizontal,
secteurs de gemmes, soufre repérable, bassin supérieur, ISS et rosace aux huit
couleurs du Bugrock. ISS avec cartes horizontales, salle et mosaïque adaptées
au rayon du terrain. Profil `discovery`, monde neuf ; contrôles natifs,
265 GameTests et assemblages réussis. Solo préparé à 32 chunks.
## beta.182 — galerie inférieure et rosace
[Nouvelle scène de laboratoire](docs/origin-gallery-beta182.md) : galerie
naturelle sous le centre, trois salles à plusieurs niveaux, emplacement
d’obsidienne ; cloche vitrée au-dessus du Bug Rock et huit axes dégagés.
Nouveau profil `gallery`, monde neuf, contrôles natifs, 265 GameTests et
assemblages réussis. Solo à 32 chunks visité par le créateur, qui valide sa palette et sa rosace.
## beta.181 — origine à Y=320
[Premier repère central](docs/origin-landmark-beta181.md) : petite rose des vents
+23
View File
@@ -1,5 +1,28 @@
# Backlog Sanctuary
## WG-RESTORE-191 — annuler les plateaux et leurs affleurements
La visite beta.190 ne valide pas le relief corrigé. [Retour exact à la base
beta.188](restore-relief-beta191.md), pour la densité et la géologie naturelle.
Conserver les aménagements indépendants ; réserver les futures singularités
de relief à quelques endroits. Nouveau profil `original` ; 134 480 densités
comparées à l’identique, génération/réouverture et 265 GameTests réussis.
## WG-NATURE-190 — revenir au relief organique
Retour beta.189 : [contrat beta.190](natural-refinement-beta190.md). Relief local
adouci, sortie d’étang large et proche, coffres de fer enchanté dans les ruines.
Profil neuf de laboratoire ; génération, réouverture, 265 GameTests et assemblages
validés. Solo Vulkan ouvert à 32 chunks.
## Réserve d’expansion — grandes surfaces ciselées
Conserver la variante de terrasses beta.189 comme piste pour une autre expansion :
grands plateaux, falaises étagées, profondeurs et verticalité variables selon
l’identité de l’île. Ne plus appliquer ces niveaux partout sur Sanctuary Island.
Aucune expansion activée. Le portail à remplissage avec ressource renouvelable
reste à concevoir ; les perles d’Ender sont une possibilité, pas une décision.
## WG-ECO-177 — écologie liée au relief
Branche `codex/relief-ecology-beta177`. Nouveau solo à 32 chunks demandé
+85
View File
@@ -0,0 +1,85 @@
# WG-ERODED-192 — couronnes et faces du massif
Branche `codex/eroded-massif-beta192`, Minecraft 26.3, graine de visite 42.
Profil `eroded`, preset neuf `sanctuary_test:eroded_massif_v1`.
## Retour et résultat attendu
Après la restauration beta.191, les captures du 30 septembre à 22:02–22:05
montrent des marches de terre et d’herbe sur les sommets arrondis. Le créateur
souhaite de vrais changements de forme : quelques dessus plus plats sous les
arbres, des faces plus franches, des creux d’érosion et des pentes conservées.
Ne pas reproduire les terrasses périodiques 189 ou simplement peindre la roche.
## Variante
Trois couronnes sont choisies dans les points hauts du massif existant. Leur
altitude dépend du terrain original. Les trois cerisiers réservés occupent ces
couronnes, avec exclusion des points de plantation encore trop raides. Une coupe légèrement inclinée rabote le
sommet ; une face plus raide regarde son versant descendant. Les empreintes
ont des limites adoucies et irrégulières ; les petites entailles entre couronnes
s’appuient sur la distance aux deux centres de Voronoï les plus proches.
Les coupes retirent au plus 24 blocs du plafond géométrique local. Une cavité
préexistante découverte par la coupe peut donner un sol visible plus bas.
C’est une érosion géométrique locale sur le bruit 3D existant, sans simulation
physique. Pas de grille de niveaux Y, de remplissage des cavités ni de lissage
global. Aucun changement de densité à Y≤240, dans les îlots aériens ou hors du
massif central. Les formes originales restent la base sous le plafond sculpté.
La couverture végétale du massif n’est posée que sur les surfaces capables de
la porter : les fortes pentes gardent la géologie réelle (stone, gisements,
strates). Le gradient est mesuré sur des colonnes du massif en coordonnées
monde, traversant les limites des chunks et ignorant les îlots aériens. Les
hauteurs et pentes sont mises en cache dans un domaine borné. Cette variante
n’utilise pas le solveur d’hydrologie ; les étangs et déversoirs existants restent
responsables de l’eau.
Ancres, donjon, gemmes, soufre, ruines à coffres, galerie, rosace et palais sont
conservés. La règle de roche historique garde son comportement lorsque son
nouveau champ optionnel `slope` est absent. Les anciens presets gardent leurs
paramètres ; aucun monde existant n’est modifié ou régénéré.
## Vérifications et limites
Génération native finale et réouverture réussies, graine 42 :
`build/eroded192e-cold.json` et `build/eroded192e-warm.json`.
Les hauteurs mesurées sont identiques après rechargement ; les trois troncs
sont relus dans les chunks sauvegardés.
- 1 008 colonnes modifiées dans le relevé espacé de deux blocs ; 392 échantillons
de sommet peu pentus. Trois couronnes et trois cerisiers contrôlés en blocs réels.
- 39 366 densités profondes et aériennes identiques à la base ; comparaison
supplémentaire hors du massif. Aucun niveau Y périodique réintroduit.
- 1 545 échantillons rocheux contre 8 de terre/herbe parmi les fortes pentes
contrôlées. Les plantations trouvent un sol de pente ≤0,75 dans leur chunk.
- Ancres, 49 cartes/cadres, rosaces, galerie, quatre coffres de ruines, donjon,
gemmes, soufre et étangs contrôlés. Cascade large et absence d’arbres dans
les colonnes d’eau et de berge contrôlées conservées.
Démarrage final : 12,126 s à froid et 0,613 s à chaud, zéro région d’hydrologie ;
83,610 s à froid et 18,501 s à chaud avec
l’ensemble des contrôles et le remplissage de l’atlas. Ces contrôles ne tournent
pas au lancement du solo. Les mesures intermédiaires pendant la compilation
concurrente ne servent pas de référence de performance.
`./gradlew check build assemblePack assembleTestPack` réussi en 9 min 9 s,
265 GameTests réussis. Après ajustement du placement des cerisiers et de son
contrôle de réouverture, `:sanctuary-test:check :sanctuary-test:build` réussi et
pack de test réassemblé : 175 classes et 121 ressources vérifiées dans le JAR,
identique à sa copie distribuée (`build/eroded192-pack-receipt.json`).
La validation technique ne vaut pas validation esthétique du créateur. Cette
itération vise le massif central de la graine de visite ; elle ne remodèle pas
les récifs aériens. Pas de publication ni de mise à jour de l’installation Prism.
## Visite
Nouveau solo `Sanctuary-Eroded-Massif-192-Solo`, run `visite192`, profil `eroded`,
graine 42, vue 32 et simulation 12, spectateur et commandes autorisées.
Position préparée (94,316,153), face au massif. Les 49 cartes et cadres ainsi
que les huit ancres éteintes et l’origine sans relais ont été relus dans cette
copie neuve. Options et shader repris du solo 191, fermé et sauvegardé à 22:14.
Client Vulkan lancé et entrée dans le monde confirmée à 22:34 ; vue 32,
simulation 12 et visibilité de l’ascension contrôlées dans
`build/eroded192-solo.log`.
+83
View File
@@ -0,0 +1,83 @@
# STEM-185 — ligne fine, portail de toiture et vitres reliées
Ticket du 30 septembre 2026, branche `codex/fine-stem-beta185`.
Le créateur valide la fleur et le palais beta.184, ainsi que la galerie inférieure.
Il demande de supprimer l'escalier, les paliers et la grosse colonne : une ligne
d'un bloc doit guider visuellement le bateau-poule vers le palais. Compléments
pendant la réalisation : incruster le portail horizontal au sommet du dôme,
libérer l'espace au-dessus de la carte, fermer le jour sous les vitres supérieures.
## Contrat
Nouveau profil de labo `stem`, preset `sanctuary_test:fine_stem_v1`, réglages
`sanctuary_test:fine_stem_v1_10`, graine de visite **42**, version **beta.185**.
La dimension 1600, le champ de bruit 640, l'île et les eaux retenues restent
ceux de beta.184. Aucun changement de format, d'ancien preset ou de sauvegarde.
La visite beta.184 a été arrêtée proprement à la demande du créateur ; une
nouvelle sauvegarde est préparée pour la relance.
## Résultat
- Rosace et Bugrock toujours en (0,640,0), palette et huit axes conservés.
- **Une seule colonne de calcite en (0,Y,0)** au-dessus du dôme inférieur,
de Y=652 jusqu'au départ de l'évasement à 1160. Pas de spirale, de marches,
de plateforme intermédiaire, de balustrade ou de pont d'escalier.
- Les huit nervures s'élargissent progressivement près du sommet, en partant
du même bloc central. Fleur, balcon et pièce du palais à Y=1278 conservés.
- Portail horizontal 7×7, ouverture 5×5, **incrusté à Y=1309 dans le dôme**.
Son cadre remplace les vitres sommitales et touche la toiture sur tout son
pourtour extérieur. Le passage central traverse la voûte ; plus de cadre
suspendu au milieu de la salle, au-dessus des 49 cartes au sol.
- Un soubassement de calcite ferme le bloc d'air sous les panneaux de verre.
Les huit passages restent ouverts. Leur dégagement est vérifié avec les
collisions natives, pour une coque standard de 1,375 bloc et son pilote.
- Les moteurs du bateau-poule existant ne comportent pas de plafond fixe à
640. Aucun changement de mécanique de vol ni de comportement de familier.
La révélation de la couronne en montant reprend beta.184. La galerie inférieure,
son portail 5×5, le soufre, les secteurs miniers, les étangs et le donjon restent
ceux validés. Les portails restent des infrastructures, sans voyage dimensionnel.
## Vérifications et visite
Contrôle natif final `solo185c/stem/42` réussi : **508 tranches d'un seul bloc**,
16 approches libres (huit par lieu, coque et pilote légèrement au-dessus des
tapis de mousse), 104 colonnes vitrées appuyées sur calcite, 24 blocs de cadre
reliés à la toiture. Les 16 868 entrées du plan correspondent aux blocs générés.
La voûte hors de l'ouverture centrale reste identique à beta.184. Aucune marche
ni palier dans cette variante. Reçu : `build/stem185-quick-result.json`.
L'île conserve ses 567 échantillons de densité vérifiés, zéro région d'hydrologie,
les trois cerisiers, cinq spawners, quatre wagonnets à butin, la galerie, le soufre
et les trois secteurs de gemmes. Le serveur démarre en 5,2 s ; préparation avec
sondages, atlas et contrôles en 42,3 s. Ce dernier temps inclut les tests.
Lecture NBT du monde arrêté : 49 cartes complètes et figées, cadres horizontaux
et identifiants uniques. Ancien cadre suspendu en air, nouveau cadre de toiture
en obsidienne, tige centrale isolée et soubassement du vitrage confirmés.
Reçus : `build/stem185-saved-atlas.json`, `build/stem185-saved-blocks.json`.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch`
réussi en **7 min 3 s**, **265/265 GameTests**. Journal :
`build/stem185-check-build.log`. Le daemon de compilation a été arrêté pour
libérer la mémoire avant le client Vulkan.
Solo Vulkan ouvert le 30 septembre à **07:32:51**. Nouveau monde
`visite185/stem/42`, `Sanctuary-Fine-Stem-185-Solo`, créatif, commandes et vol,
vue 32 chunks et simulation 12. Départ préparé dans le palais supérieur.
Entrée confirmée en (6.5,1280,8.5), vue 32 et simulation 12 dans le journal
`build/stem185-solo.log`. Vérification native du frustum réussie à 07:32:53 :
couronne cachée depuis le sol, révélée en arrivant à la rosace. Inspection de
la fenêtre du jeu : carte au sol présente et vitrages reliés au soubassement.
L'ancienne sauvegarde beta.184 reste conservée. Aucun canal public ou instance
Prism modifié ; seuls les artefacts locaux et le laboratoire sont livrés.
Les contrôles de passage ne
remplacent pas un trajet intégral piloté en bateau-poule.
| Lieu | Téléportation |
| --- | --- |
| Rosace du Bugrock | `/tp 0.5 639 5.5 180 -20` |
| Palais et carte au sol | `/tp 6.5 1280 8.5 140 35` |
| Portail dans la toiture | `/tp 0.5 1312 0.5 180 75` |
| Liaison fine, vue extérieure | `/tp 24 920 24 135 -20` |
| Portail inférieur | `/tp 0.5 177 7.5 180 30` |
+101
View File
@@ -0,0 +1,101 @@
# WG-GALLERY-198 — création Large interrompue par la galerie centrale
Branche `codex/gallery-generation-beta198`, depuis beta.197. Minecraft 26.3,
Fabric 0.19.5, Java 25. Retour R013 et journal `message-11.txt` fourni par le
créateur : arrêt Windows à la création Large, graine `4736390610738281858`.
## Cause et correction
L'exception `No supported central gallery floor` dans `Cavern183.create`
interrompt la décoration du premier chunk. Le repli beta.197 exige encore de
la roche exactement en (0,0), sous la surface de cette même colonne. Sur cette
graine, son sommet n'est qu'à Y=168 et presque toute la colonne traverse du
vide, alors que les colonnes voisines possèdent des assises. L'erreur a été
reproduite sur macOS avec la même graine avant modification. Ce journal ne
montre pas une panne du pilote Vulkan ni un manque de mémoire.
Les nouveaux profils beta.198 conservent d'abord toutes les recherches
existantes. Un dernier repli ne s'exécute que lorsqu'elles échouent : il compare
l'assise et la couverture sur l'emprise de la chambre, de Y=32 à Y=276, sans
condition éliminatoire sur la seule colonne centrale. Au plus 12 250 sondes,
une seule planification mise en cache par source de biomes. Même un puits
entièrement vide conserve la salle et sa fondation locale existante, plutôt
que d'annuler la création du monde. Dans ce cas limite, la chambre peut être
ouverte sur le vide : ce correctif ne prétend pas créer une cavité fermée dans
une roche absente.
Le portail reste horizontal en X=0, Z=0. Les dimensions de la chambre,
maçonnerie et règles du relief, de l’eau, des biomes et des minerais restent
identiques.
Aucun calcul régional d'hydrologie ajouté.
## Sauvegardes et distribution
`stable_gallery198` est un paramètre de codec additif, faux par défaut.
Les trois nouveaux profils `shared_island198_{small,medium,large}` et le preset
public Sanctuary l'activent. Les profils 196/197 restent présents et gardent
leur comportement ; leur personnalisation conserve leur révision exacte.
Les protections de spawn introduites en 197 restent actives en 197 et 198.
Le pack de test est destiné à un **nouveau monde**. Une création interrompue
beta.197 ne migre pas automatiquement en beta.198 : recréer un nouveau monde
avec la même graine et la taille souhaitée. Aucun monde personnel modifié,
aucun chunk régénéré, aucune publication du canal ni installation Prism.
## Vérifications
- Échec beta.197 reproduit dans `build/worldgen-lab/gallery198-baseline/`.
- Tests synthétiques réussis : puits vide, appuis autour d'un centre vide,
fine couche haute, roche pleine, déterminisme et borne de calcul.
- Matrice native réussie : Small/Medium/Large, graines 0, 17, 42,
`4736390610738281858` et `-9223372036854775808`. Plans répétés à l'identique ;
toutes les galeries admises par beta.197 conservent exactement leur plan.
Le nouveau repli s'active pour la graine du journal, dans les trois tailles.
- Small, Medium et Large sur la graine fournie : création puis réouverture
réussies. Portail en `(0,183,0)`, trois salles accessibles, respectivement
1 927, 1 925 et 1 930 cellules accessibles ; géométrie et ornements identiques
après reprise. Démarrages serveur à froid : 18,4 / 24,2 / 21,1 secondes ;
ces mesures locales ne sont pas un temps de chargement garanti sous Windows.
- Client intégré **Vulkan**, vue 8 chunks et mémoire Java 2 Gio : Large sur
la graine exacte, arrivée réelle en `(-15.5,254,-15.5)`, huit recherches
natives de spawn sur l’île. Parcours FR/EN des trois tailles, annulation,
aller-retour disque et maintien des révisions historiques 196 et 197.
Réussi en 1 min 2 s, trace `build/gallery198-client.log`.
Commande du contrôle ciblé, rejouable dans un nouveau dossier :
```sh
python3 scripts/worldgen_lab.py verify --profile large \
--seed 4736390610738281858 --run galerie-regression-neuve \
--checks gallery198 --memory 2048
```
Les traces finales sont dans `build/worldgen-lab/gallery198-targeted/`.
Le mode normal `--checks full` conserve toutes ses assertions. Son premier
passage dans `gallery198-fixed/` a démarré correctement puis s'est arrêté sur
un **autre contrôle de laboratoire** : le secteur émeraude contient 171 blocs,
dont 120 exposés, contre le minimum attendu de 200. Ce seuil est uniquement
vérifié par le banc de test, pas pendant une partie ordinaire. Ni l'assertion
ni les minerais ne sont modifiés ici. La vérification complète de toutes les
décorations sur cette graine reste donc ouverte ; seuls les contrôles ciblés
explicitement indiqués sont déclarés réussis.
Aucun essai effectué directement sous Windows ; le défaut du journal est
reproduit avec le même générateur natif et la même graine sur cette machine.
`./gradlew check build assemblePack assembleTestPack -PsanctuaryFocusedTests=menus`
réussi en **4 min 27 s**, 139 tâches : contrôles purs dont le nouveau repli,
quatre GameTests menus et assemblages. Journal : `build/gallery198-check-build.log`.
Le parcours client utilise une instance de développement et ne produit pas de
campagne de captures.
Export local **Sanctuary-Test-beta.198.mrpack**, **12 282 084 octets**.
SHA-256 : `647498a0da3330ce0483e65fe6510ec8d09b6975365261fe59a57d61f3ef0688`.
Archive ZIP, versions, dépendances et classes de chaque JAR vérifiées contre
le build. Toutes les anciennes ressources de données restent identiques à
beta.197, sauf le preset public qui sélectionne maintenant Medium beta.198.
Les nouvelles ressources activent explicitement le correctif ; les anciens
profils ne le font pas. Les tests natifs finaux correspondent à l'empreinte
finale des sources de génération. Reçu : `build/gallery198-artifact.json`.
Copie et guide dans `Downloads/`, sans sauvegarde, réglage personnel ni journal
inclus dans l'archive.
+125
View File
@@ -0,0 +1,125 @@
# ANCHORS-186 — rosaces ornementées et huit refuges
Ticket du 30 septembre 2026, branche `codex/hidden-anchors-beta186`.
Retour sur beta.185 : retirer les barrières supérieures, conserver la silhouette,
ajouter de fins liserés au verre et intégrer huit ancres dans le terrain de leur
secteur de couleur, en surface, en grotte et sur les récifs.
## Contrat
Nouveau laboratoire `anchors`, preset `sanctuary_test:hidden_anchors_v1`,
graine de visite 42, beta.186. Génération déterministe, recherche bornée dans
le champ de densité existant et écritures limitées au chunk en décoration.
Aucune régénération des visites précédentes ni modification des anciens presets.
Les huit couleurs reprennent la palette du Bugrock, dans le même ordre
que les rosaces. Les ancres conservent le bloc et ses propriétés existantes.
Les dépôts distincts restent locaux à ce prototype : ils allument l'ancre et
sa gemme sur le Bugrock ; ils ne génèrent pas encore d'expansion. Les coûts
sont des matériaux accessibles sur l'île, sans or ni redstone.
## Réalisation
Le balcon supérieur perd ses murets. Deux liserés alternent calcite et cuivre
patiné sur chaque dôme, en remplaçant une mince bande de verre. Huit petits
losanges ponctuent les grandes verrières. Le volume, les couleurs dominantes,
la tige d'un bloc, le Bugrock à Y=640, le portail de toiture et l'atlas restent
ceux de beta.185. Les passages sur les huit axes restent dégagés.
La recherche échantillonne une grille finie et les grands récifs existants.
Elle exige une base rocheuse et un débouché naturel praticable. Deux secteurs
aériens sont retenus lorsqu'ils disposent de récifs compatibles ; les autres
privilégient alternativement une grotte ou la surface, avec repli sur un autre
emplacement naturel admissible du même secteur. Le choix varie avec la graine.
Le manque de site sûr fait échouer ce laboratoire explicitement plutôt que de
fabriquer une ancre au-dessus du vide. Seule la graine de visite est certifiée.
Les refuges sont de petites poches irrégulières : sol légèrement ondulé,
reliefs dans la roche présente, accès court décalé, crêtes érodées, mousse,
azalées et verre de couleur au piédestal. Certains reliefs cachent une source
de lumière, d'autres restent sombres. Les réservations évitent la galerie,
le donjon, le soufre et les bassins connus. Elles empêchent aussi la décoration
d'un chunk voisin de reboucher les volumes, sans bloquer les actions des joueurs.
Aucun calcul d'hydrologie régionale ni recherche par tick n'est ajouté.
| Secteur | Couleur | Dépôt en main |
| --- | --- | --- |
| Nord | Bleu clair | 32 pierres |
| Nord-est | Vert clair | 16 bûches de chêne |
| Est | Vert | 16 blés |
| Sud-est | Orange | 16 lingots de cuivre |
| Sud | Rouge | 16 charbons |
| Sud-ouest | Magenta | 8 éclats d'améthyste |
| Ouest | Jaune | 8 lingots de fer |
| Nord-ouest | Cyan | 16 verres |
Clic droit sur l'ancre : le serveur valide le site, le matériau et la quantité.
Un dépôt exact allume l'ancre et le bit de couleur associé au Bugrock. Les
états restent dans les propriétés natives des blocs ; aucune nouvelle base de
progression ou migration. Les textes FR/EN existants sont réutilisés. Le
profil 173 et ses anciens coûts ne sont pas modifiés.
## Vérification
Premier contrôle natif réussi sur `solo186b/anchors/42` : huit ancres, deux
récifs aériens, deux grottes et quatre surfaces/flancs. Plan déterministe de
8 650 entrées dans 26 chunks, calculé en 2,3 s. Huit dépôts vérifiés dans un
ordre non séquentiel ; tous les bits sont indépendants, les mauvais dépôts et
les répétitions conservent les objets. Les états initiaux sont restaurés après
ce test, avant la sauvegarde de visite.
Palais : 144 murets retirés, 384 blocs de liserés/losanges incrustés,
4 944 blocs de verre conservés, seize approches libres. Le volume et le cadre
du portail restent inchangés. Les huit teintes existent dans les deux rosaces.
La première réouverture a révélé un ancien contrôle de cerisiers basé sur un
compteur de génération en mémoire. Le contrôle lit désormais les troncs déjà
sauvegardés lorsqu'il n'y a pas de compteur ; aucun arbre n'est régénéré.
Validation finale sur `solo186c/anchors/42` réussie à froid puis après
réouverture : 567 échantillons de densité conservés et zéro région d'hydrologie.
Démarrage serveur 6,9 s à froid / 0,66 s à chaud ; prêt avec l'ensemble des
sondages et contrôles en 44,7 s / 6,4 s. Le plan des huit refuges prend 2,28 s
à froid. Ces mesures incluent du travail de vérification et ne constituent pas
une promesse de temps de chargement du client.
Lecture de la sauvegarde arrêtée : huit ancres de secteurs 0 à 7, éteintes,
Bugrock allumé avec masque de relais nul ; 49 cartes sauvegardées, complètes,
figées, et cadres horizontaux uniques. Aucun dépôt de contrôle ne reste activé
sur le monde proposé au créateur.
Reçus ignorés : `build/anchors186-quick-result.json`,
`build/anchors186-warm-result.json`, `build/anchors186-saved-blocks.json`,
`build/anchors186-saved-atlas.json`.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch`
réussi en **6 min 49 s**, **265/265 GameTests**. Journal
`build/anchors186-check-build.log`. Le daemon de compilation a été arrêté avant
le client pour libérer la mémoire. Solo Vulkan ouvert le 30 septembre à **08:08:19**, vue 32 et simulation 12
confirmées dans `build/anchors186-solo.log`. Contrôle de visibilité de la couronne
réussi à 08:08:20. La fenêtre du jeu confirme le balcon sans murets et les
vitrages reliés au soubassement. La visite est laissée au créateur sans téléportations d’inspection supplémentaires. L'aspect des huit
refuges reste à apprécier ensemble en jeu. Aucun canal public ni instance
Prism n'a été modifié.
## Visite — graine 42
Solo neuf `visite186/anchors/42`, `Sanctuary-Hidden-Anchors-186-Solo`,
vue 32 chunks, simulation 12, créatif, vol et commandes ; départ dans le
palais supérieur. Sauvegarde beta.185 conservée. Les emplacements suivants
sont des raccourcis de test, pas des marqueurs visibles dans le jeu.
| Secteur | Milieu | Lumière du refuge | Accès de visite |
| --- | --- | --- | --- |
| Nord | Surface / flanc | Lumineux | `/tp -71.5 243 -226.5 0 0` |
| Nord-est | Surface / flanc | Sombre | `/tp 96.5 186 -82.5 0 0` |
| Est | Récif aérien | Lumineux | `/tp 213.5 444 21.5 180 0` |
| Sud-est | Surface / flanc | Sombre | `/tp 156.5 161 193.5 0 0` |
| Sud | Grotte | Lumineux | `/tp -83.5 115 215.5 180 0` |
| Sud-ouest | Surface / flanc | Sombre | `/tp -107.5 252 181.5 0 0` |
| Ouest | Grotte | Lumineux | `/tp -96.5 223 24.5 90 0` |
| Nord-ouest | Récif aérien | Sombre | `/tp -102.5 404 -171.5 90 0` |
Palais : `/tp 6.5 1280 8.5 140 -15` ; Bugrock : `/tp 0.5 639 5.5 180 -20`.
+128
View File
@@ -0,0 +1,128 @@
# DISCOVERY-183 — lieux et ressources de Sanctuary Island
Ticket du 30 septembre 2026, branche `codex/island-discovery-beta183`.
Le créateur a visité le solo beta.182 et valide la palette naturelle, la rosace,
les étangs et la génération sous l'île. Il demande un portail **horizontal**,
les secteurs miniers et le soufre manquants, l'ISS séparée et un exemple d'eau
haute qui suit le dénivelé. Son complément relie les vitres de la rosace aux
huit couleurs du Bugrock et à une géographie lisible, froide au nord, chaude au sud.
## Contrat
Nouveau preset facultatif `sanctuary_test:discovery_v1`, profil `discovery`.
Graine de visite : **42**. Anciens presets, sauvegardes et chunks conservés.
La source de biomes possède une option `discovery` absente/false pour les
anciens profils : seule la nouvelle génération active la poche de soufre.
Aucune migration, expansion, activation de portail ou progression ajoutée.
Minecraft 26.3, blocs de soufre natifs et minerais Sanctuary existants.
## Contenu
- Galerie centrale agrandie, toujours taillée dans la roche, avec ses deux
annexes et passages en boucle. Cadre d'obsidienne de **5 × 5**, trou central
de **3 × 3**, fond un bloc plus bas, sec. Le gardien viendra ultérieurement.
- Rosace : positions, nervures, passages et Bugrock à Y=320 conservés.
Seules les vitres changent. Ordre du dessin source, depuis le nord :
bleu clair, vert clair, vert, orange, rouge, magenta, jaune, cyan.
Le petit sommet blanc conserve le rappel du logo central blanc.
Cette rose colorée donne une convention géographique ; elle ne transforme
pas encore les biomes de l'île en huit régions ni n'allume les relais.
- Trois secteurs de gemmes : saphir au nord (0,-176), émeraude à l'est
(176,0), rubis au sud (0,176), chacun de rayon 64 blocs autour des centres
de chunks. Petits amas sur les faces de grotte, entre Y=40 et 287,
toujours au moins 13 blocs sous la surface, au plus douze blocs par chunk
retenu. Les blocs exposés et stocks sont comptés dans
le monde effectivement généré. Diamants, cuivre, charbon, fer et lapis
gardent leur distribution précédente ; aucun or ni redstone ajouté.
- Une caverne de soufre au sud-ouest, altitude choisie dans la roche,
soufre/cinabre/calcite, ouverture vers le jour et trois formations de soufre
enracinées à la surface pour la repérer. Pas de lave ni de nouveau mob.
- Un lac supérieur supplémentaire et son bassin aval, reliés par un chenal
en marches sur quatre blocs de dénivelé. La partie aval s'ouvre au jour.
Les deux systèmes d'étangs beta.180 restent présents. Recherche locale
bornée et plan mémorisé ; aucune hydrologie régionale calculée.
- ISS entre Y=575 et 591 : trois modules métalliques reliés et praticables,
quatre ailes solaires, poutre centrale, antenne, porche d'arrivée et
observation vitrée vers l'île. Architecture originale, sans schematic
externe. La salle nord contient une table de cartes dans des cadres lumineux
horizontaux. Le rayon du générateur détermine la mosaïque et la salle :
5 × 5 pour le petit format (512), 7 × 7 pour le moyen (724), 9 × 9 pour le
grand (1024). Ce profil de terrain reste au format moyen ; les autres
dimensions de station sont vérifiées géométriquement, sans ajouter de
nouveaux profils de terrain à cette livraison. Aucun familier spécial,
quête ou portail vers les hauteurs fonctionnel ajouté.
Toutes les nouvelles écritures appartiennent au chunk en cours de décoration.
Plans locaux calculés une fois, aucune reconstruction dans une sauvegarde
ouverte. Les cartes sont des objets Minecraft natifs, persistés avec leurs
cadres. Une file finie les prépare par tranches de quatre lignes avec un budget
cible de 3 ms par tick. Projection topographique de la graine au pas de quatre
blocs, palette réelle pour les chunks déjà chargés, biomes pour les autres :
c'est une vue d'ensemble du terrain initial, pas une photographie détaillée
de toutes les constructions. Aucun chargement de chunks éloignés demandé pour
la carte. Les cartes terminées sont figées comme une trace de l'ancienne
expédition, et ne sont ni recréées ni regagnées si un joueur retire un cadre.
Une interruption reprend une carte inachevée avec son identifiant existant.
## Vérifications et visite
Monde neuf final `solo183e/discovery/42`, serveur arrêté et sauvegardé proprement :
567 échantillons de densité conservés sous Y=216, zéro région d'hydrologie,
trois cerisiers, les deux bassins antérieurs et les cinq spawners/quatre caches
conservés. Galerie : 1 939 surfaces reliées sur 1 979 colonnes, cadre 5 × 5 et
fosse sèche 3 × 3 vérifiés. Rosace : géométrie inchangée, huit couleurs présentes
et huit approches libres.
Stocks réellement comptés dans les trois districts : 235 saphirs (176 exposés),
624 émeraudes (441 exposées), 576 rubis (402 exposés). Nouveau système d'eau :
4 311 blocs d'eau reliés et débouché extérieur vérifié. La ventilation du soufre
est ouverte jusqu'au terrain local du débouché, plutôt qu'à la hauteur plus
basse du centre de la poche.
ISS : 4 742 entrées de plan, trois modules reliés, quatre ailes solaires,
49 cartes distinctes et supportées dans 49 cadres horizontaux. Les géométries
5 × 5, 7 × 7 et 9 × 9 couvrent les rayons 288, 395 et 544 avec leurs marges de
terrain. La mosaïque courante couvre 896 × 896 blocs. Calcul de son aperçu :
2 039 ms au total ; aucun chargement de chunk distant dans ce calcul. Lecture
NBT après arrêt : 49 cartes pleines et figées, 49 identifiants uniques, tous
les cadres orientés vers le haut, centres jointifs et aucun marqueur de travail
inachevé. Assemblage des pixels sauvegardés inspecté visuellement ; il ne s'agit
pas d'une capture du jeu. Rapport local `build/discovery183-saved-atlas.json`.
Démarrage serveur mesuré à 4,4 s ; préparation avec sondages, génération des
zones visitées et contrôles terminée à 41,1 s. Ce dernier chiffre inclut les
tests synchrones, ce n'est pas un temps de démarrage normal du solo.
Rapport `build/discovery183-quick-result.json`.
`./gradlew check build assemblePack assembleTestPack` réussi en 7 min 20 s,
**265/265 GameTests** (`build/discovery183-check-build.log`). Après les deux
retouches finales limitées au laboratoire (ventilation et lecture des couleurs
réelles de la carte), compilation, contrôles du mod de labo, assemblages et
export du lanceur ont été refaits ; les checks des trois modules inchangés,
déjà réussis, n'ont pas été répétés dans cet assemblage final. Journal
`build/discovery183-final-assembly.log`. Le monde `solo183e` vérifie ce code final.
Solo neuf `visite183/discovery/42`, `Sanctuary-Discovery-183-Solo`, vue 32,
simulation 12, créatif, vol et commandes. Départ préparé devant la table de
l'ISS. Entrée en jeu confirmée dans le journal le 30 septembre à 03:11:11 :
KokaLab en (4.5,580,-12.5), vue 32. Passage en spectateur à 03:11:48.
Client Vulkan actif.
Aucune sauvegarde antérieure ni instance Prism modifiée.
| Lieu | Téléportation de visite |
| --- | --- |
| Salle des cartes ISS | `/tp 4.5 580 -12.5 140 35` |
| Porche ISS | `/tp 0.5 577 28.5 180 0` |
| Rosace | `/tp 0.5 319 5.5 180 0` |
| Portail inférieur | `/tp 0.5 177 7.5 180 30` |
| Poche de soufre | `/tp -152 180 152` |
| Indice de soufre en surface | `/tp -158 281 149` |
| Bassin supérieur | `/tp -96 254 -192` |
| Saphirs | `/tp -62 236 -204` |
| Émeraudes | `/tp 114 172 -30` |
| Rubis | `/tp -60 192 146` |
Graine 42 testée en génération native ; les autres graines restent à éprouver.
Palette, volumes, positionnement des cartes et proportions de l'ISS à valider
en visite. Les cartes sont une vue d'ensemble figée de l'île principale,
pas encore un atlas vivant des expansions.
+78
View File
@@ -0,0 +1,78 @@
# DETAILS-188 — fonds d'étangs, soufre vivant et palettes des ancres
Ticket du 30 septembre 2026, branche `codex/living-details-beta188`.
Le créateur valide l'orientation des ancres et les formes des bassins beta.187.
Il demande des refuges adaptés au milieu et à la couleur, des fonds sédimentaires
végétalisés sans arbres dans l'eau, et les pics/geysers du biome de soufre.
Nouveau profil `details`, preset `sanctuary_test:living_details_v1`, graine 42.
Aucune modification des anciennes sauvegardes ou générations. Même densité,
mêmes bassins et mêmes emplacements/orientations d'ancres que beta.187.
Livraison locale de laboratoire ; objectif de visite avant 9 h.
## Réalisation
Géométrie et orientation des refuges conservées. Terre, herbe et terre stérile
pour la surface ; tuff, deepslate et calcite sous roche ; calcite et diorite
sur les récifs. Les incrustations reprennent la teinte du secteur. Le verre
reste au-dessus de l'améthyste, du côté de l'expansion.
Les bassins sont remplis avant les arbres. Leur lit existant reçoit des plages
cohérentes d'argile, boue, sable et gravier, sans agrandissement ni coque ajoutée.
Les colonnes de plantation immergées sont réservées pendant la décoration.
Herbes marines et quelques touffes de kelp occupent les fonds ; les matériaux
meubles exigent un support rocheux. Contours et niveaux restent ceux de 187.
Le soufre reçoit des stalagmites et stalactites natives, avec des longueurs
variables, et des sources périodiques : magma, soufre puissant, eau source.
Les volumes humides réservés, le donjon et les refuges sont évités. Les entités
de bloc sont explicitement créées et sauvegardées pour activer les minuteries
natives des geysers. Le contrôle vérifie l'entité et le ticker après réouverture.
Table de butin propre à ce nouveau preset : une ou deux pièces en fer par
wagonnet, parmi quatre armures, épée, pioche, hache et pelle. Enchantement par
la fonction native, puissance de table 12 à 20, sans enchantement trésor.
Les gemmes et les ressources déjà présentes restent dans le butin. Les anciens
wagonnets et tables ne changent pas. La progression du futur monde minéral
reste une intention, hors de cette livraison.
## Vérifications
Premier monde : les huit refuges gardent exactement leurs positions et axes.
Quatre sédiments, 611 colonnes végétalisées et zéro tronc dans les bassins.
Plan de 496 spires et 39 geysers ; 138 blocs de pic et huit sources contrôlés.
32 tirages de butin : toujours une ou deux pièces enchantées en fer, les huit
types d'équipement observés. Génération finale `solo188c/details/42` puis réouverture réussies, avec contrôle
des entités de geyser et de leurs tickers natifs. Les blocs et entités sont
relus dans la sauvegarde arrêtée : huit ancres éteintes, Bugrock allumé sans
relais, 49 cartes complètes et 49 cadres horizontaux. Les quatre wagonnets sauvegardés portent tous la nouvelle table
`sanctuary_test:chests/mine_cache_188` ; reçu `build/details188-saved-loot.json`.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch`
réussi en 7 min 54 s : 265 GameTests réussis. Les contrôles natifs finaux passent
après la correction des entités de geyser, à froid et après réouverture.
Le JAR du labo a ensuite été reconstruit et le pack optionnel resynchronisé
avec `scripts/test_pack.py` : ses 146 classes correspondent aux classes compilées,
sa table de butin correspond à la source et le JAR distribué est identique.
Lors des 32 tirages après réouverture, 51 pièces enchantées couvrent les huit
types d'équipement. Reçu de la suite générale : `build/details188-check-build.log`.
Solo `visite188c/details/42`, monde `Sanctuary-Living-Details-188-Solo`, ouvert
sous Vulkan : connexion à 09:00:41, vue 32 et simulation 12 confirmées dans le
journal, puis passage du joueur en spectateur à 09:00:58. L'objectif avant 9 h
a donc été dépassé. Le contrôle visuel des décors et des éruptions reste à faire
pendant la visite ; la présence des blocs, entités et tickers est vérifiée.
## Raccourcis de visite
- Bassin ouest : `/tp -104.5 269 119.5 180 35`
- Soufre et geysers : `/tp -217.5 159 -16.5`
- Autre source : `/tp -201.5 150 8.5`
- Wagonnet du donjon : `/tp 45.5 144 60.5`
- Ancre nord : `/tp 36.5 250 -168.5 180 0`
- Ancre sud, grotte : `/tp -71.5 176 205.5 0 0`
- Ancre est, récif : `/tp 214.5 444 16.5 -90 0`
Nouveau solo uniquement, vue 32, simulation 12, créatif avec vol et commandes.
Aucune publication du canal public ni modification de Prism.
Mesures du labo final : démarrage 12.839 s à froid / 0.912 s après réouverture ; prêt avec tous les contrôles en 71.525 s / 14.114 s. Mesurées pendant la suite générale, hors chargement graphique. Zéro région d’hydrologie. Reçus ignorés `build/details188-final-cold-result.json` et `build/details188-final-warm-result.json`.
+50
View File
@@ -0,0 +1,50 @@
# UI-194 — retirer Amis du menu principal
Branche `codex/main-menu-beta194`, depuis beta.193, Minecraft 26.3.
Le créateur suspend les retouches du terrain et les tests de screenshots,
puis reprend le cadrage des interfaces. Le retrait d’Amis est la première
modification demandée, déjà prévue dans [Storyquest](storyquest-beta173.md).
## Résultat
Le menu principal propose Solo, Multijoueur, puis Options et Quitter sur une
même ligne. Cette dernière suit Multijoueur avec l’espacement natif de 24 unités
GUI. Le bouton Amis n’est plus créé à la place de Realms ; l’entrée Realms et
la petite icône sociale restent retirées. Les commandes restantes gardent
leur ordre clavier et la version Sanctuary reste affichée.
Le parcours de démonstration, dépourvu de Multijoueur, conserve sa disposition
antérieure. Les autres accès sociaux et paramètres du compte ne sont pas
modifiés. Aucun nouveau libellé : les commandes natives restent traduites FR/EN,
les anciennes clés `sanctuary.menu.friends` sont conservées pour compatibilité.
Le menu pause, Habitant, Combat, Factions et Storyquest restent des suites
à concevoir. Aucune source de génération, donnée de biome ou sauvegarde n’est
modifiée par ce lot. Aucune nouvelle série de captures n’est lancée.
## Vérifications
Le parcours natif existant `Title045ClientChecks` est actualisé pour vérifier
l’absence d’Amis et de Realms, la disposition sans ligne vide, l’ordre clavier
et l’ouverture d’Options en FR/EN aux quatre réglages GUI. Ses captures et son
ancien parcours vers la liste d’amis sont retirés.
Validation réussie : `./gradlew check build assemblePack assembleTestPack
:sanctuary:runClientGameTest -PsanctuaryFocusedTests=menus
-PsanctuaryClientTests=true -PsanctuaryTitleClientTests=true
-PsanctuaryClientGraphicsBackend=vulkan`.
Journal : `build/menu194-check-build.log`. Les contrôles purs et les quatre
GameTests serveur ciblés passent. Le parcours client Vulkan passe en FR/EN,
sans capture, avec ordre clavier, absence des boutons retirés et ouverture
d’Options vérifiés. Dans la fenêtre 854 × 480 du test, les réglages GUI 3 et 4
sont plafonnés par Minecraft à l’échelle effective 2.
Les packs normal et de test sont assemblés. La comparaison des JAR beta.193 et
beta.194 confirme que seule `TitleMenuMixin.class` change dans le code livré ;
les données de génération sont identiques. Le module Demeure embarqué ne change
que de version dans ses métadonnées. Reçu : `build/menu194-artifact.json`.
Le labo beta.193 déjà ouvert n’est ni arrêté ni mis à jour par cette livraison.
Livraison locale uniquement ; publication et mise à jour du jeu personnel
ne sont pas demandées.
+115
View File
@@ -0,0 +1,115 @@
# ASCENT-184 — rosace à 640 et palais des hauteurs
Ticket du 30 septembre 2026, branche `codex/natural-ascent-beta184`.
Le retour sur beta.183 rejette l'ISS métallique visible depuis le sol et les
nouveaux bassins supérieurs ovales en marches. La direction retenue est une
machine naturelle : rosace, longue tige parcourable, couronne de palais comme
un parapluie, carte au sol et portail horizontal au-dessus. Dernière correction
explicite du créateur : **la rosace est à Y=640**, pas à 320.
## Contrat de génération
Nouveau profil de laboratoire `ascent`, preset `sanctuary_test:ascent_v1`,
réglages `sanctuary_test:ascent_v1_10`, graine de visite **42**. Nouvelle dimension
`sanctuary_test:ascent_1600`, constructible de Y=0 à 1599. Minecraft 26.3 accepte
ce type de dimension dans la génération native vérifiée. Le bruit du terrain
reste calculé entre 0 et 639 ; la décoration écrit les structures plus haut
dans les sections du nouveau monde. Aucun bruit supplémentaire dans le ciel
vide, aucune hydrologie régionale, aucun changement des sauvegardes précédentes.
Les presets 180–183 restent distincts. Le nouveau profil reprend l'île, ses
minerais, biomes, cavernes, soufre et donjon, mais ne place ni l'ISS métallique
ni les bassins `Water183`. Les étangs souterrains beta.180 précédemment validés
restent présents. Pas d'expansion, migration, nouveau mob ou portail fonctionnel.
## Construction et découverte
- Rosace et Bugrock translatés à **(0,640,0)**, sol à 638. Les huit couleurs et
approches de la rosace sont conservées ; la sortie est rejoint l'escalier.
- Tige de tuf, deepslate, calcite et mousse, nervures enroulées, haltes latérales.
Escalier continu avec marches natives, de 638 à 1278 : 1 923 positions de
parcours, environ 1,9 km de chemin pour 640 blocs gagnés. Largeur généralement
de trois blocs, resserrée aux virages. Le parcours depuis l'île jusqu'à la
rosace reste à concevoir ; le test permet le vol et le spectateur.
- Palais : nervures déployées à partir de 1160, sol à **1278**, voûte à **1310**.
Huit pétales vitrés, passages minéraux, cuivre oxydé, balcons. Pas de panneaux
solaires ni de modules métalliques. Le Bugrock reste uniquement sur la rosace.
- Atlas natif au sol, 49 cartes sur ce terrain moyen, dans des cadres horizontaux.
Dimensions 5×5, 7×7 et 9×9 pour les rayons déjà pris en charge. Identifiants et
marqueurs propres à ce nouvel atlas ; l'atlas 183 reste inchangé.
- Portail supérieur : cadre horizontal 7×7 d'obsidienne, coins en obsidienne
pleureuse, vide central 5×5, à 1287 au-dessus des cartes. Le portail inférieur
conserve son cadre 5×5 et son creux 3×3 à Y=175. Ce sont des infrastructures.
À 32 chunks, la caméra native peut garder un plan lointain de 2 048 blocs :
la hauteur seule ne suffit pas à dissimuler la couronne. Dans cette dimension
uniquement, le frustum refuse les volumes entièrement au-dessus de 1152 lorsque
la caméra est sous 560. La couronne devient donc éligible à l'affichage pendant
l'approche de la rosace ; les règles normales de visibilité restent applicables.
C'est une révélation côté rendu, pas une génération conditionnelle à un joueur.
Un contrôle à l'entrée du client teste les deux API natives de frustum, depuis
le sol et depuis 640. La couronne est aussi au-delà du volume maximal des ombres
Sanctuary depuis le plateau (distance 256, extrusion solaire comprise). Cela
ne supprime pas l'éclairage Minecraft inhérent aux blocs placés dans une colonne.
## Eau dans les creux existants
Recherche locale et bornée dans le champ réel de densité. Chaque candidat est
rempli par parcours des cellules d'air adjacentes sous un niveau horizontal ;
un chemin sortant du volume de recherche, vers le vide ou une zone réservée
fait rejeter le candidat. On monte le niveau par essais bornés tant que la roche
retient l'eau. Aucun mur de retenue, fond d'argile artificiel, marche ou excavation
n'est dessiné. Seules les cellules encore en air reçoivent de l'eau à la décoration.
Les cavités existantes, roches saillantes et végétaux conservés dictent le contour.
Première génération native `solo184a`, graine 42 : 45 essais, plan en 852 ms,
10 402 blocs d'eau dans la grande poche (surface de 1 045 cellules à Y=234),
739 blocs dans la seconde (290 cellules à Y=242). Aucun débouché latéral/inférieur
non retenu détecté après la décoration. Ce sont des eaux retenues ; aucune
simulation de bassin versant ou de rivière à longue distance n'est introduite.
## Vérification et visite
Premier contrôle natif réussi : 567 échantillons profonds identiques, zéro région
d'hydrologie, trois cerisiers, cinq spawners, quatre caches à butin, galerie,
soufre et trois secteurs de gemmes présents. Les 56 246 entrées du plan du palais
correspondent aux blocs générés ; toutes les positions de montée ont un support
et deux blocs libres. Les 49 cartes sont uniques, remplies, figées et supportées.
Serveur démarré en 5,3 s ; prêt en 43,6 s avec génération des sondages et contrôles.
Ce dernier temps inclut les tests et ne mesure pas un démarrage normal du solo.
Validation finale : `./gradlew check build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch` réussi en 6 min 54 s, **265/265 GameTests**.
Journal `build/ascent184-check-build.log`. Nouveau monde final `solo184b/ascent/42`
contrôlé et arrêté proprement : serveur en 5,25 s, prêt avec contrôles en 41,63 s ;
plan d'eau en 806 ms, 10 401 et 739 blocs d'eau retenus sans fuite. Le nombre de blocs d'eau diffère d'une cellule entre les deux essais ;
les niveaux et surfaces des plans restent identiques.
Rapport `build/ascent184-quick-result-final.json`.
Lecture NBT du monde arrêté : 49 cartes complètes, figées, identifiants uniques,
49 cadres orientés vers le haut à Y=1279. Air confirmé aux anciens centres
(0,320,0) et (0,576,0), bloc originel à (0,640,0), plancher d'atlas en calcite
et portail supérieur creux. Reçus `build/ascent184-saved-atlas-final.json` et
`build/ascent184-saved-blocks-final.json`.
Solo neuf `visite184/ascent/42`, `Sanctuary-Ascent-184-Solo`, commandes, créatif,
vol, vue 32 et simulation 12. Entrée client Vulkan confirmée le 30 septembre,
contrôle natif du frustum réussi à **03:45:07** : couronne cachée à Y=250,
rosace visible et couronne révélée à Y=640. Journal `build/ascent184-solo.log`.
Le profil local KokaLab de ce monde utilise la couleur Ciel et le familier de
labo par défaut. Le daemon de compilation a été arrêté pour libérer la mémoire.
Aucune sauvegarde antérieure ni instance Prism modifiée. Les archives locales
sont assemblées ; aucun canal public n'est avancé.
| Lieu | Téléportation de visite |
| --- | --- |
| Rosace et départ de la montée | `/tp 0.5 639 5.5 180 -20` |
| Palais et carte au sol | `/tp 6.5 1280 8.5 140 35` |
| Vue extérieure de la couronne | `/tp 45 1295 45 135 10` |
| Grande cavité en eau | `/tp -144 236 -96` |
| Seconde cavité en eau | `/tp -16 245 128` |
| Portail inférieur | `/tp 0.5 177 7.5 180 30` |
Cette première silhouette reste à évaluer en visite, notamment le rythme de
l'ascension et les proportions du palais. La graine 42 est la référence vérifiée ;
la disponibilité de grandes cavités sur d'autres graines reste à éprouver.
+100
View File
@@ -0,0 +1,100 @@
# CAVES-187 — soufre naturel, ancres orientées et bassins visibles
Ticket du 30 septembre 2026, branche `codex/natural-caves-beta187`.
Retour beta.186 : coque de soufre artificielle, pylônes mal orientés et verre
latéral, bassins de surface invisibles. Le relief et les trois secteurs de
gemmes sont appréciés et conservés.
Nouveau profil `natural`, preset `sanctuary_test:natural_caves_v1`, graine 42.
Les anciens presets et sauvegardes restent inchangés. Le laboratoire ajoute
un climat de soufre en trois dimensions dans la roche existante, une approche
en mousse tournée vers le centre pour chaque ancre et des pylônes opposés,
vers l'expansion, portant le verre au-dessus de l'améthyste. L'eau de surface
est recherchée dans des dépressions réellement ouvertes au ciel ; toute fuite
vers le vide fait rejeter le bassin. Aucun calcul régional d'hydrologie.
Le climat de soufre utilise un bruit volumique et respecte la priorité des
marais humides. La palette forme des dépôts cohérents de soufre, cinabre et
calcite dans les blocs rocheux existants. Musique et couleurs du biome natif
26.3 sont conservées. Les carvers, minerais et structures vanilla ne sont pas
importés : l'île conserve ses cavités et sa distribution de ressources.
L'ancienne coque et ses cheminées ne sont plus placées dans ce preset.
Les pylônes portent améthyste puis verre, sur la même verticale. Leur porte
pointe vers la direction exacte du secteur, y compris les diagonales. Le
chemin en mousse est du côté opposé. La sélection vérifie le support naturel
de la porte et du chemin ; les huit emplacements changent pour respecter
cette orientation. La galerie, les rosaces et le palais restent conservés.
L'eau de surface monte dans un creux par parcours prioritaire : chaque voxel
est échantillonné une fois à la hauteur minimale qui le rend accessible.
Une fuite, une structure réservée ou la limite de recherche arrête la montée.
Les surfaces retenues doivent être majoritairement ouvertes au ciel. Aucun
fond ajouté ni découpage elliptique ; les arbres voisins ne peuvent reboucher
les cellules d'eau réservées. Les étangs souterrains appréciés sont conservés.
L'atlas projette aussi les nouvelles eaux et perd le faux repère jaune de la
cheminée supprimée. Les recherches sont bornées, en génération uniquement.
## Contrôles natifs
Graine 42, `solo187e/natural/42` : génération à froid réussie. Recherche des
bassins en 2,674 s, contre 21,546 s au premier essai ; mêmes trois positions,
niveaux et volumes retenus. Démarrage serveur 9,881 s, prêt après l'ensemble
des contrôles en 50,190 s. Ces durées ne mesurent pas le démarrage graphique.
Zéro région d'hydrologie et 567 échantillons de densité profonde conservés.
| Bassin | Niveau | Surface totale | Surface sous ciel ouvert | Blocs d'eau |
| --- | --- | --- | --- | --- |
| Ouest, près de (-104, 88) | 248 | 1 994 | 1 475 | 11 935 |
| Nord-est, près de (120, -136) | 244 | 839 | 536 | 2 329 |
| Sud, près de (8, 136) | 254 | 1 321 | 904 | 9 446 |
Aucune fuite native détectée. Quatre points de visite confirment le biome
sulfureux, de la roche minéralisée et des cavités déjà présentes dans la densité.
Les huit refuges passent le contrôle d'orientation et de verre vertical.
Les dépôts distincts activent les huit gemmes ; les tests restaurent ensuite
les ancres éteintes et le Bugrock allumé sans relais. Les secteurs de gemmes,
les cinq spawners et quatre wagonnets à butin restent vérifiés.
Réouverture réussie : démarrage serveur 0,618 s, prêt avec vérifications en
8,226 s. Les huit ancres sont relues dans la sauvegarde éteintes et le masque
du Bugrock vaut zéro ; 49 cartes complètes, figées, avec 49 cadres horizontaux
uniques. Reçus ignorés : `build/natural187-final-cold-result.json`,
`build/natural187-final-warm-result.json`, `build/natural187-saved-blocks.json`,
`build/natural187-saved-atlas.json`.
`./gradlew check build assemblePack assembleTestPack :sanctuary-test:exportDuoLaunch`
réussi en **7 min 10 s**, **265/265 GameTests**. Journal :
`build/natural187-check-build.log`. Daemon Gradle arrêté avant le client.
Solo Vulkan rejoint à **08:40:37**, départ (-104,5 ; 269 ; 119,5), vue 32
et simulation 12 confirmées dans `build/natural187-solo.log`. Contrôle client
de visibilité verticale réussi à 08:40:39. Le créateur a pris la main et est
passé en spectateur ; aucune téléportation d'inspection supplémentaire.
L'appréciation visuelle des nouveaux bassins et du soufre reste celle de
cette visite ; les mesures ci-dessus sont des vérifications natives.
Seule la graine de visite est certifiée ; pas de migration des anciennes
sauvegardes ni de modification du canal public ou de Prism.
## Visite — graine 42
Nouveau solo `visite187/natural/42`, `Sanctuary-Natural-Caves-187-Solo`.
Vue 32, simulation 12, créatif, vol et commandes. Départ au-dessus du bassin
ouest ; les anciennes visites restent conservées.
| Lieu | Raccourci de visite |
| --- | --- |
| Bassin ouest | `/tp -104.5 269 119.5 180 35` |
| Bassin nord-est | `/tp 120.5 258 -116.5 180 35` |
| Bassin sud | `/tp 8.5 270 156.5 180 35` |
| Soufre, cavité haute | `/tp -215.5 219 -35.5` |
| Soufre, cavité profonde | `/tp -203.5 150 -11.5` |
| Soufre, cavité nord-ouest | `/tp -191.5 152 -179.5` |
| Ancre nord, approche intérieure | `/tp 36.5 250 -168.5 180 0` |
| Ancre nord-est | `/tp 193.5 251 -84.5 -135 0` |
| Ancre est, récif | `/tp 214.5 444 16.5 -90 0` |
| Ancre sud-est | `/tp 157.5 226 145.5 -45 0` |
| Ancre sud, grotte | `/tp -71.5 176 205.5 0 0` |
| Ancre sud-ouest | `/tp -96.5 252 181.5 45 0` |
| Ancre ouest | `/tp -96.5 230 0.5 90 0` |
| Ancre nord-ouest, récif | `/tp -102.5 404 -160.5 135 0` |
+77
View File
@@ -0,0 +1,77 @@
# WG-NATURE-190 — relief organique, déversoirs et butin des ruines
Branche `codex/natural-refinement-beta190`, graine de visite **42**, Minecraft 26.3.
Nouveau profil de laboratoire `refined`, preset `sanctuary_test:natural_refinement_v1`.
Les sauvegardes précédentes restent des témoins ; aucune régénération ni migration.
## Contrat de cet essai
Après la visite beta.189 : supprimer les terrasses systématiques du relief principal,
conserver quelques inflexions locales douces, des affleurements suivant la pente et
sans masque en cases de quatre blocs. Les captures du 30 septembre à 20:44 et 20:45
montrent le relief principal ; les îlots aériens ne sont pas la cause des paliers.
Comparer les débouchés voisins des étangs au lieu de retenir le premier rayon ;
privilégier une sortie courte, large de quatre ou cinq blocs et peu creusée.
Conserver les cavités, le portail et les ruines appréciés. Remplacer la table de
craft de chaque ruine par un coffre de matériel en fer enchanté.
Aucun calcul d’hydrologie régionale ajouté ; travail local borné, décoration existante.
## Réserve de conception, non implémentée
- Expansion à grandes surfaces étagées et falaises ciselées : conserver la variante
beta.189 comme référence, avec une profondeur et une verticalité adaptées au thème.
- Portails horizontaux : remplissage progressif avec une ressource renouvelable.
Perles d’Ender, source Endermen ou butin sont des pistes ; ressource, nombre,
recette et destination ne sont pas décidés. Aucun mécanisme d’activation ajouté ici.
## Vérifications
Compilation et génération/réouverture natives réussies, graine 42 :
`build/refined190b-cold.json` et `build/refined190b-warm.json`.
- 704 des 745 sommets échantillonnés gardent exactement la hauteur naturelle
antérieure aux terrasses ; 41 épaules retouchées, écart maximum 3 blocs.
30 246 densités des profondeurs et des îlots aériens identiques. Les corniches
fines sont conservées ; une première interpolation qui pouvait les supprimer
a été corrigée avant la visite.
- Une sortie d’étang de cinq blocs de large, depuis X=150, Y=244, Z=-157 à -153
vers X=154–155. Chenal de 4–5 blocs, dont une partie dans l’étang ; huit blocs
retirés. Bord de chute irrégulier, 64 blocs préremplis puis écoulement natif.
La recherche compare les ouvertures proches, reste bornée à dix blocs et
deux cascades, et renonce si aucune bouche assez large ne convient.
- 1 052 blocs de plage sèche, 434 plantes aquatiques, zéro tronc dans les
colonnes contrôlées des plages et étangs. Fonds : 1 337 sable, 249 gravier,
2 832 argile. Le choix du chenal ne crée pas de fond ou barrage artificiel.
- Quatre ruines conservées, coffres en (25,164,-63), (-7,148,-79),
(65,164,-79), (57,121,-79). Couvercles dégagés, table de butin conservée
après réouverture. Une ou deux pièces en fer par coffre, enchantements
de table de niveau 12–20 : armure, épée, pioche, hache ou pelle.
32 tirages vérifiés à froid, puis 32 à chaud ; aucune pièce non enchantée.
- Huit ancres, cartes du palais, gemmes, soufre, donjon et galerie contrôlés.
Ni nouvelle dimension ni activation des portails ajoutée.
Démarrage natif : 12,331 s à froid / 0,673 s à chaud ; avec toute la batterie
et le remplissage intégral des cartes : 81,505 s / 26,656 s. Plans locaux
berges/déversoirs : 174 / 170 ms. Zéro région d’hydrologie calculée.
Mesures obtenues pendant la suite de tests générale, sans garantie de durée
identique sur chaque machine. Le solo ne lance pas ces contrôles lourds.
`./gradlew check build assemblePack assembleTestPack` réussi en **9 min 22 s**,
avec **265 GameTests réussis**. Après les derniers correctifs validés par le labo
natif, le JAR optionnel a été réassemblé et le pack de test resynchronisé.
Ses classes et ressources correspondent aux fichiers compilés, et sa copie
assemblée est identique : `build/refined190-pack-receipt.json`.
Aucune publication de canal ni mise à jour Prism. La validation esthétique reste
celle de la prochaine visite ; les contrôles structurels ne la remplacent pas.
## Visite
Solo `Sanctuary-Natural-Refinement-190-Solo`, run `visite190`, profil `refined`,
graine 42. Copie neuve du monde de diagnostic arrêté, sauvegardes antérieures
préservées. Spectateur et commandes activés, vue 32, simulation 12, Vulkan.
Connexion confirmée à **21:01:12**, position (178,260,-146), face à la cascade.
Les 49 cartes et cadres horizontaux sont présents, complets et verrouillés ;
les huit ancres et le Bugrock sont revenus à l’état initial après les contrôles.
`build/refined190-saved-atlas.json`, `build/refined190-saved-blocks.json` et
`build/refined190-solo.log` conservent ces vérifications.
+88
View File
@@ -0,0 +1,88 @@
# EXP-202 — étagement du Nord et village enneigé
Branche `codex/north-ecology-beta202`, depuis beta.201. Retour R021 et trois
captures du labo 201 examinées (12:11:22, 12:11:46 et 12:12:57, 4 octobre 2026).
Conserver les volumes, les cavités et les deux bassins 201, appréciés par le
joueur. Refaire uniquement leur écologie et leurs sols : taïga, vallées ouvertes,
podzol, épicéas géants natifs, neige progressive avec l'altitude, glaciers sur
les hauteurs. Fin des aplats indépendants et du camouflage glace/glace bleue.
Glace de cave limitée aux cavités protégées par une épaisseur de roche dans
chaque direction ; aucune couche glacée appliquée aux falaises ou au dessous.
Les cinq igloos doivent trouver une plaine réellement enneigée avant placement.
Les pics de glace restent vanilla, comme demandé en R020.
Nouveaux profils 202 et journal distinct, diamètre nordique 1024. Aucune migration
ni régénération d'un monde existant ; 200 et 201 gardent leurs générateurs.
L'île de départ et les autres directions restent inchangées. Le chantier est
isolé de la visite 201 encore ouverte. Validation native neuve Small/42, contrôles
numériques sur 0, 42 et 4736390610738281858 ; pas de campagne de captures.
## Réalisation
Le champ de densité et les deux bassins délèguent exactement à la génération
201. Seuls les sols, la végétation, la couverture neigeuse et le placement du
village changent. La limite de neige suit l'altitude, avec un versant nord plus
froid. Son épaisseur progresse de quelques couches à un bloc après la végétation.
Les glaciers sont réservés aux hauts versants ; aucune glace bleue ne peint
la surface. Les vallées restent en herbe, les grands boisements en podzol.
La taïga ordinaire et les épicéas géants réemploient les features 26.3 de taïga
et `old_growth_spruce_taiga`. Aucun arbre géant custom ajouté. Les pics utilisent
toujours `minecraft:ice_spike` et `minecraft:ice_patch`, sans remplacement des
features natives. Les matériaux de cave glacée nécessitent du recouvrement et
une assise rocheuse autour de la colonne ; les marges et dessous restent rocheux.
Les cinq igloos cherchent une zone sèche, entièrement enneigée et peu pentue,
avec classement par dénivelé. Aucun disque blanc ajouté sous le village.
Caches numériques bornés ; pas de simulation hydrologique supplémentaire.
## Vérifications du 4 octobre 2026
- `north202Smoke` : neuf couples graine/taille (0, 42, 4736390610738281858 ;
512/724/1024), égalité des colonnes et densités avec 201, présence de taïga,
vallées, neige progressive et glacier minoritaire, protection du dessous.
Recherche du village vérifiée aussi sur les graines 1 à 100. Toutes passent.
- Ressources historiques de génération inchangées, sauf le preset public qui
sélectionne désormais 202 et l'ajout du biome 202 au tag des ours polaires.
Les trois noise settings de l'île de départ sont identiques aux fichiers 201.
- Essai natif final : `build/worldgen-lab/north202-native-b`, Small, graine 42,
diamètre de l'expansion 1024. Offrande réelle, interruption à 0/58 chunks,
reprise, cinq igloos, une cave de guérison, relais de continuation orienté Nord.
- Échantillon de taïga : 1517 blocs de troncs, 7677 blocs de feuilles,
troncs atteignant 27 blocs, 13 empreintes de troncs larges, 3339 blocs de podzol.
Pics natifs : 2910 blocs de glace au-dessus du terrain, maximum sondé 43 blocs.
Ces nombres décrivent des échantillons, pas toute l'expansion.
- Village : 333 des 339 colonnes sondées hors maisons portent de la neige.
Lac : 8995 blocs de glace et 41795 blocs d'eau, sans fuite dans l'air.
Caverne sondée : 49 blocs glacés et 2585 blocs d'air. Bordure sondée sur
16 directions : 340 blocs rocheux, aucune glace compacte/bleue.
La validation esthétique reste celle de la visite. Pas de nouvelle campagne de
captures. Visite native Small/42 uniquement : Windows et Medium/Large natifs
ne sont pas vérifiés ici. Les sept autres directions restent provisoires.
## Livraison locale
`./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=operator,realtime -PsanctuaryAtlasOnly=true` : succès,
143 tâches, dont 126 exécutées ; sept GameTests ciblés et les smokes du projet.
La suite native historique complète n'a pas été relancée (limite documentée
pour beta.200). Logs : `build/north202-check-build.log` et
`build/north202-native-final.log`.
MRpack : `build/Sanctuary-Test-beta.202.mrpack`, copie identique dans Downloads
avec `Sanctuary-Test-beta.202-Guide.txt`. ZIP, versions 26.3 / Loader 0.19.5,
hashes Fabric API, deux JAR et leurs 2258 classes compilées vérifiés.
Reçu local : `build/north202-artifact.json`.
Taille : 12407853 octets ; SHA-256 :
`b5ecdfcb2c4b1988f9402653a16c823b50f0661e8b1db0f0e7d6c21e9841f6f8`.
Aucun monde inclus dans le MRpack. Aucun canal public ni instance Prism modifié.
Visite neuve `Sanctuary-North-202-42`, issue de l'essai natif final, créatif,
commandes activées, Vulkan, vue 32, simulation 4. Départ en vol près du village.
Village `(-212,191,-1292)`, taïga géante `(144,215,-976)`, lac `(-247,258,-765)`,
pics vers `(-208,281,-880)`, relais `(-128,202,-1240)`.
Ouverture confirmée à 12:41:46 par `NORTH202_VISIT_OPEN`, joueur
`(-160,228,-1247)` ; log `build/north202-visit-client.log`.
+120
View File
@@ -0,0 +1,120 @@
# EXP-200 — premier Nord glacial
Branche `codex/north-expansion-beta200`, depuis beta.199. Retours R015–R017.
## Résultat visé
Un continent nordique de diamètre nominal 2048 : crêtes, pics, vallées de taïga
neigeuse, neige poudreuse localisée, cavernes de glace et lacs gelés fermés.
Chèvres et ours polaires natifs de Minecraft 26.3 (choix confirmé).
Un village de plusieurs igloos ; un seul contient le laboratoire souterrain
vanilla avec villageois zombie. Pas d'autre famille de structures dans cette
première tranche. Le Sud, l'Est, l'Ouest et les diagonales seront travaillés à part.
## Placement et ouverture
L'identité du relais reste celle de son secteur. Recherche proche dans un
éventail de ±15° devant l'ancre, sans basculer dans une autre direction.
La première version 200 porte le Nord à 2048 ; les sept autres reliefs gardent
leur taille/profil provisoire de 199. Le relais de continuation du Nord conserve
la direction, le climat et la taille du Nord.
Toute l'emprise est vérifiée vierge, réservée durablement et publiée avant les
travaux. À cette échelle, `ready` signifie accès et relais préparés ; le reste
est généré nativement pendant l'exploration. L'annonce SGA décrit cette ouverture,
pas une pré-génération intégrale. Préparation bornée, sans simulation hydrologique
globale ni réutilisation silencieuse de chunks déjà explorés.
## Contrat des sauvegardes
Nouveaux profils `shared_island200_small/medium/large`, journal distinct
`sanctuary-lab-expansions200`. Aucun monde existant modifié ou migré ; les profils
et ressources 196–199 restent inchangés. IDs historiques conservés. Les essais
activent uniquement des expansions dans des mondes de développement neufs,
graine 42 puis relevés déterministes sur 0 et 4736390610738281858.
## Vérifications prévues
Direction et diamètre, déterminisme, cavités, confinement des eaux, village
et laboratoire habitable, vraie activation, sauvegarde et reprise. Compilation,
`check build` et assemblages. Ne pas confondre vérification technique et validation
esthétique : le rendu reste une première version à visiter.
## Implantation et contrôles intermédiaires
Le champ nordique compose des crêtes déformées, des sommets secondaires et des
vallées, puis soustrait des passages et chambres 3D. Cache numérique borné par
worker ; aucune lecture de chunks voisins pour dessiner le terrain. Six biomes
nordiques reprennent l'écologie native de 26.3 ; les sources d'eau/lave dispersées
et salles de monstres vanilla sont retirées de ces seuls profils. Les bassins
ont un fond rocheux continu et une couverture de glace, sans hydrologie globale.
Village : cinq igloos natifs orientés et ajustés individuellement au sol. Un
seul laboratoire, avec échelle, coffre vanilla, potion de faiblesse, villageois
et villageois zombie persistants. Les arbres évitent les maisons et les lacs.
La faune initiale utilise les attributs du biome à la surface réelle, car la
sélection vanilla à Y=1600 échantillonnait le vide ; règles de spawn natives
conservées. Ours polaires également admis sur la glace des lacs.
- `north200Smoke` : 9 couples graine/taille, huit éventails, relief/cavités,
bassins analytiquement fermés, journal 2048 et refus de 2048 en génération 199.
- Essai intermédiaire Small/42, 2 Gio : vraie offrande nord, interruption à
0/58 chunks, puis reprise. Village de cinq igloos et un laboratoire contrôlés ;
lac avec 3475 blocs de glace et 10525 blocs d'eau, sans voisin d'air sous l'eau ;
136 blocs de glace et 1926 blocs d'air dans les colonnes de caverne sondées.
Ce sont des sondages locaux, pas les totaux du continent.
- Le premier passage a révélé un barreau manquant sous la trappe : raccord
corrigé et vérifié lors du second passage. Les essais antérieurs restent
conservés sous `build/worldgen-lab/north200-native-a` et `north200-native-b`.
- Les 148 ressources historiques de génération du module Test restent identiques
à beta.199 (hors preset public, qui sélectionne maintenant 200).
## Essai final et livraison locale — 4 octobre 2026
Le parcours serveur final `north200-native-final/small/42` répète l'activation
réelle et l'interruption/reprise sur les sources livrées. Les 58 chunks d'accès
et de relais passent à FULL ; cinq igloos, un laboratoire, le lac et la cavité
glacée sont vérifiés. Les entités sauvegardées comprennent six ours polaires,
trois renards, huit lapins, un villageois et un villageois zombie. Aucune chèvre
observée dans ce petit échantillon : leurs règles natives sont présentes dans
les sommets et pentes, sans validation d'une rencontre dans ce parcours.
Le client Vulkan a ouvert une copie de visite neuve, `Sanctuary-North-200-42`,
avec confirmation `NORTH200_VISIT_OPEN` à `(-200,224,-968)`. Créatif, commandes,
vue 32 chunks et simulation 4 ; shaders désactivés. Le village se trouve autour
de `(-200,212,-988)`, la trappe spéciale vers `(-193,210,-961)`, le lac sondé en
`(-64,187,-1204)` et le relais suivant en `(-176,215,-2121)`.
Archive locale : `build/Sanctuary-Test-beta.200.mrpack`, également copiée dans
`/Users/koka/Downloads/`, avec `Sanctuary-Test-beta.200-Guide.txt`.
12 366 771 octets ; SHA-256 :
`2a29b35613f95540ec9d910e043a1f6f26b2dfe7d6d95c50997ba4bbde203cdc`.
Intégrité ZIP, version, dépendances Minecraft 26.3 / Fabric Loader 0.19.5,
empreintes Fabric API, deux JAR identiques au build et 2246 classes compilées
vérifiés. Les trois tailles sont présentes ; aucune sauvegarde embarquée.
Reçu : `build/north200-artifact.json`. Aucun canal public ni Prism synchronisé.
La commande complète `check build assemblePack assembleTestPack` a été lancée,
puis interrompue volontairement pendant
`retainedHydrologySurvivesNativeDecoration` (ancienne génération 24, préparation
de 112 chunks FULL). Les captures de threads montrent l'attente de génération ;
elles ne démontrent pas un interblocage. Cette suite générale n'est pas validée.
Logs : `build/north200-check-build.log`, `north200-fullcheck-threads*.txt`.
Les essais serveur nordiques sont distincts de cette suite historique.
Les sept GameTests `operator,realtime` passent avec le fixture plat
`-PsanctuaryAtlasOnly=true`, indépendamment de l'essai natif nordique.
Cette sélection ne remplace pas les 265 tests de la suite générale.
La commande `./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=operator,realtime -PsanctuaryAtlasOnly=true` termine avec
succès : 141 tâches, dont 110 exécutées et 31 à jour. Elle inclut les tests
numériques, notamment `north200Smoke`. Log complet :
`build/north200-check-build-focused.log`. Archive revérifiée après l'assemblage
final, identique aux JAR et classes finaux ; daemon de build arrêté ensuite.
Limites : validation native détaillée sur Small/42, neuf couples graine/taille
pour les contrôles numériques, pas de lancement Windows ni de visite native
Medium/Large sur cette version. Les 2048 blocs désignent le diamètre nominal,
pas une pré-génération intégrale. Le démarrage à 32 chunks a produit des retards
de ticks avec les contrôles et un autre client simultanés sur le Mac 8 Gio :
aucune conclusion de fluidité ni correction des saccades n'est revendiquée.
+87
View File
@@ -0,0 +1,87 @@
# EXP-203 — fractures, lisières et taïga géante
Branche `codex/north-fracture-beta203`, depuis beta.202. Retour R022.
Le rendu 202 est globalement validé. Passe ciblée demandée : ajouter de fortes
variations locales de volume et des trous par bruit 3D, retirer la neige ajoutée
après la végétation, brouiller les limites du podzol, donner aux grands épicéas
une hauteur pouvant atteindre 64 et varier les matières des sommets glacés.
Aucun nouveau village : la proposition supplémentaire est retirée par le joueur.
Les cinq igloos existants restent présents.
Nouveaux profils 203 seulement, Nord 1024. Aucun monde existant régénéré ni
migré ; profils 200–202 inchangés. Bassins et basses plaines d'accueil protégés
de la déformation. Pics de glace vanilla conservés. Arbres avec moteur natif,
configuration propre au biome géant ; aucune modification globale des épicéas.
Pas de simulation d'érosion ou d'hydrologie supplémentaire, caches bornés.
## Réalisation
Le champ 203 combine des déplacements verticaux locaux (deux échelles de bruit)
et un bruit volumique négatif de plus grande portée. Les nouvelles poches
peuvent traverser le toit ou le dessous, contrairement aux caves fermées 202.
Le bruit est atténué sur les basses plaines, les bords et tout autour des lacs.
Les seuils de sommet restent lisibles ; le détail 3D module les roches et glaces.
La passe de neige après décoration n'est pas appelée pour 203. Les prairies
froides gardent leur herbe ; la neige demeure comme matériau de sommet,
substrat nécessaire aux pics vanilla et matériau des igloos existants.
Le champ de podzol comporte deux fréquences de détail aux lisières. Le
`alter_ground` circulaire des nouveaux grands arbres est supprimé.
Les arbres réemploient `giant_trunk_placer` et `mega_pine_foliage_placer` de
Minecraft 26.3. Configuration locale : hauteur 32 + aléa 0–16 + aléa 0–16,
soit 32 à 64 blocs, couronne de 18–24 ; mélange de géants et de petits épicéas.
Les features `ice_spike` / `ice_patch` restent natives et inchangées.
## Contrôles
`north203Smoke` passe sur neuf couples graine/taille (0, 42,
4736390610738281858 ; Small/Medium/Large). Les déplacements locaux sondés vont
jusqu'à environ −62 / +56 blocs selon le cas. Ouvertures de surface présentes,
densité bornée, lacs et leurs coques inchangés, lisières de podzol irrégulières.
Recherche des cinq igloos vérifiée également sur les graines 1 à 32.
Les ressources worldgen historiques sont inchangées, hors sélection du nouveau
preset public et ajout du biome 203 au tag des ours polaires. Les noise settings
de l'île de départ restent identiques à 202. Aucun test visuel Windows ni
nouvelle campagne de captures : le rendu est à juger dans le labo.
Essai natif final : `build/worldgen-lab/north203-native-b`, Small/graine 42.
Vraie offrande, interruption à 0/58 chunks, reprise et relais prêts. Cinq igloos
et un laboratoire conservés. Recherche de cave orientée vers un intérieur
suffisamment épais, plutôt que le sommet le plus haut désormais fracturé.
Dans les échantillons : 4218 blocs de troncs, 10450 de feuilles, 21 empreintes
de troncs larges ; tronc le plus haut mesuré 59 blocs (plafond configuré 64).
Sommet : 309 blocs de glace bleue, 63 de glace ordinaire, 2154 de glace compacte,
1219 de neige pleine et 1694 de roche. Pics vanilla : maximum sondé 13 blocs
au-dessus du terrain. Prairie : zéro couche de neige parmi 339 colonnes.
Lac : 8995 blocs de glace et 41795 d'eau, sans fuite ; caverne sondée : 226 blocs
glacés et 1878 d'air. Bordure : 339 blocs rocheux, aucune glace compacte/bleue.
Il s'agit de contrôles locaux, pas d'un comptage de toute l'expansion.
## Livraison
`./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=operator,realtime -PsanctuaryAtlasOnly=true` : succès,
144 tâches dont 120 exécutées, sept GameTests ciblés et les smokes du projet.
La suite native historique complète n'a pas été relancée (limite beta.200).
Logs : `build/north203-check-build.log` et `build/north203-native-final.log`.
MRpack vérifié : `build/Sanctuary-Test-beta.203.mrpack`, copie identique dans
Downloads avec le guide. Archives, versions, dépendances, empreintes Fabric API,
JAR et 2265 classes compilées vérifiés ; reçu `build/north203-artifact.json`.
12433661 octets ; SHA-256 :
`ba160b4134c0b9dbe402e1834bdbec4f5561bfd6bd9dc147590dd76ee30dbf4f`.
Visite neuve `Sanctuary-North-203-42`, issue de l'essai natif final. Créatif,
commandes activées, Vulkan, vue 32, simulation 4 ; départ au-dessus de la taïga.
Village `(-212,191,-1292)`, forêt `(144,215,-976)`, pics `(-208,278,-944)`,
premier lac `(-247,258,-765)`. Aucun canal public ni instance Prism modifié.
Pas de sauvegarde embarquée dans le pack ni de revendication sur les saccades.
Essai natif Small/42 seulement ; Medium/Large sont contrôlés numériquement.
Ouverture confirmée à 13:12:20 par `NORTH203_VISIT_OPEN`, départ
`(196,292,-931)`, backend Vulkan confirmé dans `build/north203-visit-client.log`.
+90
View File
@@ -0,0 +1,90 @@
# EXP-201 — Nord compact, glacier et bosquets
Branche `codex/north-glacier-beta201`, depuis beta.200 ; retours joueur R019–R020.
## Résultat demandé
Le diamètre du Nord passe de 2048 à 1024 blocs. Remplacer la nappe blanche perçue
par un paysage de glace lisible : pics de glace, glacier, deux lacs gelés visibles,
bosquets localisés et contraste roche/glace/neige. Conserver les cinq igloos et
le laboratoire avec villageois zombie. Les lacs doivent contenir de l'eau sous
leur glace ; la végétation doit être réellement générée, pas seulement déclarée.
Les pics utilisent explicitement `minecraft:ice_spike` et `minecraft:ice_patch`
de Minecraft 26.3, avec leurs placements natifs. Aucune forme de pic recodée.
Le Nord ne doit pas devenir le catalogue de tous les biomes froids. Son identité
est le glacier habité ; les grandes forêts boréales, côtes froides et autres
paysages restent des pistes pour NO/NE. Leur répartition n'est pas décidée ici.
## Contrat de génération
Nouveaux profils 201 et journal `sanctuary-lab-expansions201`, destinés uniquement
aux nouveaux mondes. Les profils et le champ nordique 200 restent disponibles
pour les sauvegardes historiques ; aucune conversion des expansions 2048 déjà
créées, ni régénération de chunks. Île de départ Small/Medium/Large inchangée.
Placement dans l'éventail existant de ±15°. Préparation bornée de l'arrivée et du
relais, puis exploration. Les sept autres directions restent provisoires.
## Vérifications attendues
Contrôle de taille et de direction, stabilité de la génération 200, proportions
et visibilité des surfaces glaciaires, lacs fermés, relief vertical, bosquets et
village. Vérification native de vrais troncs/feuilles, glace de surface non
recouverte et pics élevés. Graines numériques 0, 42 et 4736390610738281858 ; visite
native neuve Small/42. Pas de campagne de screenshots ni d'hydrologie globale.
Les contrôles techniques ne remplacent pas le retour esthétique du joueur.
## Intégration
La 200 posait une couche de neige pendant le traitement du sol, avant les
features végétales ; les arbres natifs de taïga passent par les saplings
`spruce_checked` / `pine_checked`, qui exigent un support viable. La 201 laisse
le sol des bosquets en herbe jusqu'à leur plantation. Les lacs et le glacier ne
reçoivent plus la feature `freeze_top_layer`, afin que leur glace reste visible.
Les zones de pics conservent au contraire le bloc de neige requis par la feature
native `minecraft:spike`. Ses références et sa fréquence restent vanilla.
Le substrat compact associe une crête, des épaules glaciaires, deux champs de
pics natifs, deux bosquets et deux bassins fermés. Caches numériques bornés ;
aucune simulation hydrologique globale. Le village est rapproché du premier lac.
Le relief, les matériaux et le choix de biomes de la 200 gardent leur branche
de génération historique.
## Résultats — 4 octobre 2026
- `north201Smoke` : neuf couples graine/taille, Nord 1024, relief, parts de glace,
bosquets, lacs et champs de pics, confinement des bassins, déterminisme et
routage historique 200. `north200Smoke` reste passant. Les 155 ressources
historiques worldgen du module Test restent identiques, hors preset public.
- Serveur neuf Small/42, `build/worldgen-lab/north201-native-a` : vraie offrande,
interruption à 0/58 chunks, reprise et accès préparés. Cinq igloos, une cave
avec trappe/échelle/coffre/potion ; relais de continuation orienté Nord.
- Échantillon du bosquet : 395 blocs de troncs et 3294 blocs de feuilles
d'épicéa, répartis sur 56 colonnes de troncs (pas un comptage d'arbres entiers).
Champ de pics natifs : 1838 blocs de glace compacte au-dessus du terrain,
maximum sondé de 11 blocs. Lac : 8995 blocs de couverture gelée et 41795 blocs
d'eau ; 7602 surfaces de glace visibles contrôlées, zéro couche de neige dessus.
Caverne : 561 blocs de glace et 2585 blocs d'air sondés. Totaux locaux uniquement.
- `./gradlew check build assemblePack assembleTestPack
-PsanctuaryFocusedTests=operator,realtime -PsanctuaryAtlasOnly=true` : succès,
sept GameTests ciblés et tous les smokes associés, 142 tâches dont 118 exécutées.
La suite native historique complète n'a pas été relancée (limite beta.200).
Logs : `build/north201-check-build.log` et `build/north201-native-a.log`.
Livraison locale : `build/Sanctuary-Test-beta.201.mrpack`, copie dans Downloads
avec le guide. 12 386 623 octets ; SHA-256 :
`37925f012e4096c253f25e573305237e71114448d866a57f3fbad6e3da6b3d89`.
ZIP, dépendances 26.3 / Loader 0.19.5, empreintes Fabric API, JAR et 2252 classes
compilées vérifiés. Les trois tailles sont incluses, aucune sauvegarde embarquée.
Reçu : `build/north201-artifact.json`. Aucun canal public ni Prism modifié.
Visite préparée : `Sanctuary-North-201-42`, nouveau monde issu de l'essai natif.
Départ près du lac en `(-142,323,-705)`, créatif, commandes, Vulkan, vue 32 et
simulation 4. Ouverture confirmée par `NORTH201_VISIT_OPEN` dans
`build/north201-visit-client.log`. Village `(-128,205,-637)`, lac `(-247,258,-765)`, bosquet
`(-324,258,-780)`, champ de pics vers `(-273,-1059)`, relais `(-128,202,-1240)`.
Limites : visite native Small/42 seulement, pas d'essai Windows ou Medium/Large
natif sur cette version. Pas de revendication sur les saccades. Les paysages
NO/NE ne sont pas implémentés ; les anciens mondes ne changent pas de taille.
+68
View File
@@ -0,0 +1,68 @@
# ORIGIN-182 — galerie inférieure et rosace
Suite du retour en jeu sur beta.181, le 30 septembre 2026. Branche
`codex/origin-gallery-beta182`, dans le checkout de développement réutilisé
`rocky-ecology-beta178`. La palette naturelle de l’origine est validée par le
créateur. Les élévations gênent les huit directions ; elles doivent être
placées entre les axes. Essayer une cloche vitrée en rosace et développer
la scène souterraine. L’origine reste en l’air.
La mention « le truc dans les airs » est interprétée ici comme la rosace,
en attendant une éventuelle précision ; l’ISS reste un chantier séparé.
## Contrat du nouveau monde
Nouveau preset facultatif `sanctuary_test:gallery_v1`, profil `gallery`,
graine 42. Les profils précédents et la génération normale restent disponibles
avec leur génération propre. Aucun ancien monde, chunk ou format de sauvegarde
n’est modifié. Aucune expansion ni dimension n’est activée par ce prototype.
La galerie sélectionne une cavité centrale sous X=0/Z=0 par un échantillonnage
borné du champ de densité existant. Deux chambres voisines forment une boucle
avec elle ; les passages suivent des failles inclinées, les sols ondulent et
les plafonds sont voûtés. Roche dominante, calcite par veines, mousse ponctuelle,
quelques reliefs de tuf. Pas de murs rectangulaires plaqués sur toute la caverne.
Un socle d’obsidienne central et deux montants sombres interrompus donnent un
premier emplacement au portail inférieur. C’est une scène architecturale :
ni portail actif, ni recette d’allumage ou téléportation ne sont implémentés.
L’origine conserve la palette, le sol à huit directions et le bloc en
(0,320,0), lumineux et monochrome. Une cloche de verre clair et blanc avec
huit nervures de cuivre patiné remplace les trois élévations. Les supports
sont entre les axes ; huit ouvertures basses conservent vues et passages.
L’ISS, les huit salles d’ancres et les trois secteurs de gemmes ne sont pas
ajoutés dans ce lot.
Les deux lieux sont posés par chunk à la génération, sans ticker, sans
recherche globale d’eau et sans journal de progression. Le plan souterrain
est calculé une fois pour la source de biomes puis réutilisé. Une sauvegarde
recharge les blocs, elle ne reconstruit pas les lieux.
## Vérifications
Compilation et export du lanceur réussis. Monde neuf `solo182a/gallery/42` :
contrôles de la beta.180 conservés (567 échantillons de densité, trois cerisiers,
bassins de 1 232 et 1 221 blocs, cinq spawners et quatre caches avec les gemmes).
Zéro région d’hydrologie calculée.
La galerie contient 1 558 colonnes sculptées ; 1 514 surfaces praticables
appartiennent au même réseau, connecté par marches d’un bloc maximum.
Les trois centres de salle sont en (0,175,0), (-34,169,-26), (-38,146,24).
Le socle porte l’obsidienne en (0,177,0). La rosace compte 1 841 blocs avec
le fragment rocheux : huit axes libres à hauteur du joueur et noyau lumineux
sans relais activé. Rapport local `build/gallery182-quick-result.json`.
Arrêt du serveur de préparation et sauvegarde terminés proprement.
Solo neuf préparé sous `visite182/gallery/42`, `Sanctuary-Gallery-182-Solo`,
vue 32, créatif et commandes, première vue sur la galerie.
Visites : galerie `/tp 0.5 177 7.5 180 0` ; rosace
`/tp 0.5 319 5.5 180 0`. Rendu et proportions à juger en jeu sur la graine 42 ;
aucune autre graine ni capture du rendu n’est revendiquée.
Suite générale `check build assemblePack assembleTestPack` réussie en 8 min 1 s,
265/265 GameTests ; journal `build/gallery182-check-build.log`.
Entrée dans le solo confirmée depuis par le créateur. Le journal
`build/gallery182-solo.log` enregistre KokaLab connecté le 30 septembre à
02:25:12, vue 32 chunks. Son retour valide la palette, la rosace et les étangs ;
la correction du portail et les lieux manquants sont suivis dans
[DISCOVERY-183](island-discovery-beta183.md).
+61
View File
@@ -0,0 +1,61 @@
# WG-RESTORE-191 — revenir avant les plateaux
Branche `codex/restore-relief-beta191`, Minecraft 26.3, graine de visite 42.
Nouveau profil `original`, preset `sanctuary_test:pre_terraces_v1`.
## Contrat
Le retour beta.190 rejette toujours le relief et les affleurements ajoutés depuis
les plateaux. Restaurer exactement le champ de densité **beta.188**, avant les
terrasses beta.189 et les épaules beta.190. Retirer leurs passes d’affleurements
et leur exclusion d’arbres sur ces affleurements. La géologie d’origine retrouve
sa responsabilité : pierre, strates, gisements, cavités et pointes existantes.
Conserver les corrections indépendantes appréciées : ancres au sol, berges,
cascade large, galerie éclairée et coffres de fer enchanté dans les ruines.
Cela restaure le relief et la géologie de base, pas tous les blocs d’un monde
beta.188 puisque ces aménagements récents restent présents.
L’intention pour la suite est un récif avec creux, cavités et pointes, avec
quelques formations singulières par endroits. Aucun nouveau lissage, plateau
ni déformation généralisée dans cette remise à plat du travail.
Les variantes 189/190 restent des références conservées, hors de ce nouveau profil.
Nouveaux mondes seulement ; aucune sauvegarde existante modifiée ou régénérée.
## Vérifications
Génération et réouverture natives réussies, graine 42 :
`build/original191a-cold.json` et `build/original191a-warm.json`.
- **134 480 densités identiques** aux paramètres enregistrés de beta.188, de
Y=0 à Y=632 sur l’île et ses îlots ; aucun remappage de hauteur activé.
- Passes d’affleurements 189/190 court-circuitées dans ce profil. Leur masque
est inactif sur les 1 681 colonnes contrôlées ; la règle de géologie d’origine
et les plantations natives reprennent la surface hors des aménagements réservés.
- Comparaison au commit beta.188 `658aaf8` : corps du calcul de densité, paramètres
de bruit, îlots aériens et règle de géologie identiques. Preuve locale :
`build/original191-baseline-receipt.json`.
- Huit ancres, 49 cartes et cadres du palais, salle inférieure et éclairage,
quatre ruines avec coffres enchantés, donjon, gemmes et soufre contrôlés.
- Cascade de cinq blocs de large conservée, chenal de 4–5 blocs et huit blocs
de roche retirés ; aucune nouvelle déformation de montagne pour la produire.
Zéro tronc dans les colonnes contrôlées des bassins et berges.
Démarrage natif : 12,281 s à froid et 0,738 s à chaud ; avec tous les contrôles
et le remplissage des cartes : 79,283 s / 22,798 s. Zéro région d’hydrologie.
Ces contrôles de laboratoire ne sont pas lancés au démarrage du solo.
`./gradlew check build assemblePack assembleTestPack` réussi en **7 min 40 s**,
**265 GameTests réussis**. Le JAR est identique aux classes et ressources compilées
et à sa copie distribuée dans le pack de test : `build/original191-pack-receipt.json`.
Aucune publication ni modification de l’installation Prism. Le client Vulkan est lancé avec la nouvelle
sauvegarde `Sanctuary-Original-Relief-191-Solo`, run `visite191`, profil `original`,
vue 32 et simulation 12, spectateur, commandes autorisées, position préparée
(94,316,153) face au relief. Connexion confirmée à **21:46:39**, vue 32 et simulation 12 ; Vulkan confirmé.
Les cartes et l’état initial des ancres ont été relus dans cette copie neuve.
Les sauvegardes 188/189/190 restent conservées.
Le créateur a confirmé l’ouverture ; la connexion est vérifiée dans
`build/original191-solo.log`. Cette confirmation porte sur l’entrée dans le solo,
pas encore sur l’appréciation esthétique du terrain restauré.
+143
View File
@@ -0,0 +1,143 @@
# EXP-199 — affiner les expansions et les raccorder au laboratoire
Branche `codex/runtime-expansions-beta199`, depuis beta.198. Retour R014.
Implémentation et validation locales ; aucune publication du canal packwiz ni mise à jour de Prism.
## Contrat retenu
Réutiliser `ExpansionJournal`, `ExpansionRuntime`, les contrôles d'occupation,
les contextes de bruit natifs et les biomes d'expansion existants. Les ancres
actuelles du laboratoire doivent désormais demander une vraie expansion.
Recherche automatique sur 360°, priorité à la proximité du parent parmi les
sites admissibles, une région immuable par relais, et un nouveau relais sur
chaque région prête. Relief fin : surface large et ondulée, quelques plateaux
souples, volumes amincis en conservant les cavités du bruit 3D.
Les lectures d'admission et la préparation native sont échelonnées entre les
ticks. Annonce SGA aux joueurs seulement après les chunks FULL et le relais.
Les doubles activations ne créent ni second continent ni double prélèvement.
La sauvegarde reprend une région réservée après interruption.
## Contrat de nouveaux mondes
Les profils 196–198 gardent leurs paramètres et leurs ancres de démonstration.
Les nouveaux profils 199 initialisent un journal d'expansions propre. Aucun
ancien monde n'est migré, aucun chunk existant régénéré, aucun ancien format
réécrit. Première admission limitée aux régions réellement vierges ; même du
vide déjà généré reste protégé. Cette contrainte peut éloigner une expansion
si le joueur a déjà exploré les alentours ; ce n'est pas une promesse de
contact bord à bord. La réutilisation du vide certifié reste le mécanisme
historique distinct, à ne pas activer implicitement dans une partie.
## Ciel et performance
Rotation stellaire : sept journées Minecraft, sur l'horloge du monde faisant
autorité ; conserver la même transformation pour la visée et les constellations.
Les saccades sont également signalées à l'arrêt, y compris shader Sanctuary désactivé (confirmation du créateur). Aucune cause n'est attribuée
au worldgen sans mesure ; préserver cette enquête séparément du réglage des
expansions. Les débordements de mares restent consignés sans nouvelle refonte.
## Première tranche jouable
Le relais demande les mêmes huit offrandes distinctes que le laboratoire. Le
joueur reste à proximité, offrande en main, pendant la recherche. Les matériaux
ne sont retirés qu'après réservation durable. Une seule préparation est active
à la fois ; chaque relais ouvre une région de diamètre nominal 256 blocs, avec
un relais de continuation. Le climat suit la couleur du relais d'origine ; la
position réelle est choisie automatiquement parmi 64 angles, par distances
croissantes sur la grille de chunks. Le relief réutilise la densité 3D historique,
comprimée verticalement autour de Y=220 et déformée par son bruit cohérent.
Les biomes naturels décorent ces nappes. Les villages et forteresses vanilla ne
sont pas réactivés par l'ajout de ces biomes à la palette : leurs structures
seront une tranche distincte. Les structures et reliefs de l'île centrale sont
conservés. Pour Small/42, une recherche locale plus précise peut retrouver les
sols d'ancres manqués par la grille historique ; les réservations d'eau suivent
alors leurs emprises réelles avec marge au lieu d'exclure un secteur entier.
Le moteur conserve son journal et sa file native, avec un adaptateur réservé aux
profils 199. Admission : 32 lectures asynchrones au maximum par lot, contrôle des
chunks chargés et des sauvegardes concurrentes, mémorisation des refus
pour éliminer les candidats qui recouvrent un chunk déjà trouvé occupé, puis réservation et publication
sur le même tour serveur. Le relais suivant est placé après les chunks FULL ;
l'annonce SGA possède une infobulle lisible donnant ses coordonnées.
Limites : pas de régénération du vide déjà exploré, pas de choix de direction,
pas encore de variation du prix/taille pour les relais successifs. La limite
historique reste de 64 régions, Sanctuary incluse. Le journal et l'inventaire
joueur sont deux fichiers Minecraft distincts : la protection contre le double
clic ne constitue pas une transaction atomique entre ces fichiers lors d'une
coupure brutale au milieu d'une sauvegarde.
## Périmètre de validation
Placement proche sur tous les angles et refus des emprises occupées, densités
bornées et nappes fines, horloge stellaire cyclique, activation réelle, deuxième
relais, double interaction, interruption/reprise et notification après FULL.
`check build`, assemblages et essai client Vulkan avant livraison locale.
## Relevés de validation
- Compilation et assemblages : `check build assemblePack assembleTestPack`,
avec les suites natives `operator,realtime`, réussis (7 GameTests et contrôles
purs). La vérification du placement couvre 9 couples graine/taille : 0, 42 et
4736390610738281858 × Small/Medium/Large, les huit secteurs, l'ordre de distance,
les conflits et la réouverture du journal.
Dernier passage complet : `build/expansion199-check-build-delivery.log`,
réussi en 3 min 40 s. Compilation finale des tests client réussie également.
- Nouveau Small, graine 42, 2 Gio : serveur prêt vers 14,2 s ; première expansion
au centre (368, -368), relais Y=231, après 418 chunks FULL. Le deuxième relais
réserve son enfant environ 11 s après l'annonce du premier. L'arrêt volontaire
a lieu avec cet enfant encore réservé. Cette durée inclut les essais et n'est
pas le temps normal de création d'un monde.
- Journaux finaux : `build/expansion199-check-build-final.log` et
`build/worldgen-lab/expansion199-native-final/small/42/`. Les premiers essais
interrompus restent conservés et ne sont pas des validations.
- Réouverture du même Small/42 : enfant repris, 418 chunks FULL, relais
(912, 236, -368), une seule annonce pour cet enfant et aucune répétition de
celle du parent. Sur 81 colonnes échantillonnées, 1380 blocs non vides, entre
Y=191 et Y=275 (végétation comprise) : ce relevé local ne mesure pas le volume
total de toutes les expansions.
- Client Vulkan, Small/42 : arrivée sur l'île, synchronisation native des étoiles
et pause de l'horloge, vraie interaction du joueur avec le relais, double clic
sans double prélèvement. Un bloc d'or placé dans le premier candidat reste
intact et force l'admission d'un autre emplacement. Les interfaces FR/EN,
trois tailles, profils historiques et aller-retour disque passent également.
- Deux essais client consécutifs terminent l'expansion puis sauvegardent et
ferment normalement (`build/expansion199-client-pass-a.log` et
`build/expansion199-client.log`). Le tout premier essai avait réussi les
assertions puis rencontré une exception dans `ChunkMap.tick`, lors du suivi
des entités. Cause non isolée : un contrôle de réentrance et 100 ticks
d'observation supplémentaires n'ont pas reproduit le défaut. Ce résultat
n'est pas un correctif prouvé du crash initial. Pas d'essai direct Windows.
### Saccades : capture locale, cause non isolée
Première capture client macOS Vulkan, Small/42, vue 8 chunks, shader désactivé, joueur immobile :
1241 images relevées, médiane 31,15 ms (limitation de cadence de l'environnement
de test), P95 33,12 ms, P99 33,71 ms, maximum 98,01 ms, 7 images au-delà de 50 ms.
258 sections ont encore été compilées pendant la capture. Le JFR contient 4152
échantillons de workers dans la génération, et des pauses GC jusqu'à 88,45 ms.
Le joueur immobile ne signifiait donc pas que le chargement avait terminé.
Ce relevé n'isole pas les saccades d'une partie complètement stabilisée, et ne
prouve pas leur cause chez le créateur. Le shader n'est pas nécessaire pour
reproduire quelques images longues dans cet environnement. Aucun correctif
général des saccades n'est revendiqué dans cette livraison.
Mesures ignorées par Git :
`build/expansion199-stationary-first/stationary.json` et `stationary.jfr`.
Les captures suivantes restent sous
`mods/sanctuary/build/run/clientGameTest/diagnostics/expansion199/`.
Pas de nouvelle campagne de screenshots.
## Livraison locale
`Sanctuary-Test-beta.199.mrpack`, 12 322 965 octets, copié dans
`/Users/koka/Downloads/` avec `Sanctuary-Test-beta.199-Guide.txt`.
SHA-256 : `fd2ebe4f43bf0d455377a2f456106165ff5e3519e5c3dce4126818c03f4ae44f`.
Intégrité ZIP, manifeste Minecraft 26.3/Fabric Loader 0.19.5, dépendances externes,
JAR intégrés comparés aux classes compilées et anciennes ressources vérifiés.
Les 142 ressources historiques de génération restent identiques à beta.198 ;
le preset public vise désormais 199. Reçu : `build/expansion199-artifact.json`.
Le canal packwiz et l'installation Prism n'ont pas été modifiés.
+126
View File
@@ -0,0 +1,126 @@
# WG-SHARE-196 — laboratoire partageable, trois tailles
Branche `codex/lab-mrpack-sizes-beta196`, depuis beta.195, Minecraft 26.3,
Fabric Loader 0.19.5, Java 25. Demande : donner le dernier labo à un ami sous
forme de `.mrpack`, avec Small, Medium et Large, puis diagnostiquer la lenteur.
## Parcours et périmètre
Dans le pack **Sanctuary Test**, Solo → Créer un monde → Monde → type
**Sanctuary** → **Personnaliser** propose Small (512), Medium (724, défaut),
Large (1024). Les types vanilla restent disponibles. Le module Test fournit le
preset public ; le pack ordinaire sans ce module conserve sa génération.
L’interface ne propose plus les anciens laboratoires. Annuler et Échap ne
modifient rien ; la taille est conservée lors du rechargement des registres et
dans les réglages enregistrés. Libellés FR/EN.
Les nouveaux réglages sont `sanctuary_test:shared_island196_small`, `medium`,
`large`. Les anciens codecs et presets restent présents. Aucun monde existant
n’a été migré, modifié ou régénéré. Le preset caché `production_reference196`
conserve le témoin de l’ancienne génération pour le lanceur scientifique.
Le terrain reprend le profil `coherent` : rayon natif selon la capacité, sans
étirer les voxels. Les bornes des biomes, l’écartement des récifs, la recherche
des bassins et ancres, les secteurs de gemmes et la sortie du donjon suivent
la taille. Les pièces conservent leurs dimensions physiques. Atlas de 25/49/81
cartes ; rosace à Y=640 et palais vers Y=1280 dans la dimension haute de 1600.
Aucune nouvelle direction esthétique ni campagne de screenshots.
En Small/Large, la réservation des ancres tient compte de la hauteur : un
refuge sec peut surplomber une autre installation, sans empiéter sur son
volume. C’est nécessaire pour que le donjon ne condamne pas tout un secteur
d’une petite île. Les poches d’eau naturelles restent conditionnées à un creux
fermé : l’absence d’une poche supplémentaire ne crée pas un bassin artificiel.
Les grands bassins souterrains et les étangs de surface sont vérifiés séparément.
La graine 0 en Large a révélé une poche native de dripstone dans le sol d’un
couloir, puis des pointes sèches dans un recoin immergé d’un étang. Les nouveaux
profils protègent les sols de circulation via un index par colonne et toutes
les cellules d’eau retenues, y compris celles sous un surplomb. Les protections
historiques restent utilisées pour les anciens profils.
## Diagnostic du chargement beta.195
Le créateur a quitté la visite puis lancé un nouveau monde Large depuis le
menu. Les journaux identifient la graine `3466058668915588406` et **l’ancien
générateur**, pas le labo `coherent`. Du démarrage du serveur à la sélection
du spawn : 18:09:26 → 18:11:05, soit environ 99 secondes.
Les chronos internes indiquent notamment l’hydrologie régionale (plusieurs
millions de sondes par région), Transit24 (23,4 s), le sanctuaire minéral
(33,7 s), Heritage24 (15,5 s) et les anciennes expéditions. Certains chronos
s’imbriquent : ne pas les additionner.
Un JFR de 43 secondes a capturé ensuite la préparation des chunks de cette
ancienne génération : 2509 échantillons Java, nombreuses piles TransitPlan21
et HeritagePlan19, bruit natif et allocations de streams/maps. Les huit GC
mesurés totalisent environ 2,16 s (pas uniquement des pauses du jeu). Le nouveau
Voronoï n’apparaît pas dans ce relevé, car ce générateur ne l’utilise pas.
Un second relevé intermédiaire sur **Small, graine 0**, avant les diagnostics détaillés,
contient 1578 échantillons CPU : 1151 (72,9 %) incluent les bruits natifs,
118 (7,5 %) les recherches d’eau et 11 (0,7 %) l’érosion Erosion192, qui contient
les joints Voronoï locaux. Ces ensembles peuvent se recouvrir ; ce n’est pas
une décomposition additive du temps mural. La piste prioritaire serait de
réduire les évaluations répétées du relief et les sondages de bassins, en
comparant exactement les blocs produits. Cette optimisation n’est pas livrée
ici : le raccordement au bon générateur élimine déjà les anciens planificateurs.
Le lanceur accepte maintenant `--jfr CHEMIN` pour produire des enregistrements
séparés à froid et à chaud. Exemple :
```sh
python3 scripts/worldgen_lab.py verify --profile large --seed 0 \
--run mesure-nouvelle --memory 2048 --jfr build/profils/large.jfr
```
`serverStartedMs` mesure l’arrivée au spawn serveur. `readyForVisitMs` inclut
ensuite les diagnostics qui chargent d’autres chunks, inspectent les structures
et calculent l’atlas. Ne pas présenter cette deuxième durée comme l’attente
normale du joueur. Ces essais ne mesurent pas la fluidité Vulkan à 32 chunks.
Les JFR bruts restent ignorés et ne sont pas intégrés au pack.
## Vérifications et distribution
`./gradlew check build assemblePack assembleTestPack -PsanctuaryFocusedTests=menus`
réussi : suite des contrôles purs, quatre GameTests menus, modules et pack.
Parcours natif client Vulkan final réussi en FR/EN, sans captures : entrée
publique du labo, trois tailles, Annuler/Échap, rechargement des registres,
aller-retour des réglages sur disque et personnalisation vanilla.
Création et réouverture de la graine 0 réussies pour les trois tailles :
| Taille | Diamètre | Spawn à froid | Spawn après réouverture | Froid avec diagnostics | Cartes |
|---|---:|---:|---:|---:|---:|
| Small | 512 | 14.34 s | 0.72 s | 62.0 s | 25 |
| Medium | 724 | 19.86 s | 0.64 s | 120.2 s | 49 |
| Large | 1024 | 23.96 s | 1.26 s | 144.5 s | 81 |
Huit ancres et offres distinctes, donjon et butin, étangs sans cellules perdues,
portail inférieur, palais, soufre, ruines et secteurs de gemmes contrôlés.
Zéro région d’hydrologie lourde dans les six passages. Les chronos ont été pris
avec JFR, parfois pendant d’autres contrôles : **pas un benchmark isolé**, ni
une garantie de chargement graphique à 32 chunks. Les essais antérieurs Small
sans concurrence atteignaient environ 11 s au spawn serveur. Les autres graines
et le matériel de l’ami restent à éprouver.
Les rapports Medium/Large sont dans `shared196f`, Small dans `shared196g`.
Entre les deux, seule la résolution des identifiants de taille a été mise en
cache pour éviter les allocations dans les protections : mêmes chaînes,
capacités, données et algorithmes de génération. Après ce dernier ajustement,
contrôle/construction du module Test et réassemblage réussis ; contrôles du
mod principal déjà passés réutilisés. Une relance redondante de leur suite est
interrompue, sans être comptée comme une seconde réussite complète.
Export final `Sanctuary-Test-beta.196.mrpack`, 12 268 171 octets, copié dans
`/Users/koka/Downloads/`, avec guide séparé. SHA-256 :
`f73db69953641237399d4d3f0210ba0252e1ea45377b5ff57d6e231d67aa5988`.
Archive ZIP et manifeste vérifiés ; classes identiques aux classes construites,
trois capacités dans les ressources, dépendance Fabric API téléchargée et
vérifiée par SHA-512/SHA-1. Aucun monde, log, secret ou réglage personnel inclus.
Demeure et JEI sont imbriqués dans Sanctuary. Export local uniquement, sans
publication du canal packwiz, synchronisation Prism ou envoi à un tiers.
Traces ignorées : `build/shared196-check-build.log`,
`build/shared196-final-assemble.log`, `build/shared196-client-final.log`,
`build/shared196-artifact.json`, `build/worldgen196-profile/` et les rapports
`build/worldgen-lab/shared196{f,g}/.../{cold,warm}.json`.
+54 -1
View File
@@ -1,5 +1,58 @@
# Sanctuary Island — fil rouge de la refonte
## Retour au laboratoire — 1er octobre 2026
Le créateur demande une nouvelle visite avec la graine 0 pour décider si le
worldgen reste en pause. [WG-OPTIONS-195](worldgen-options-beta195.md) conserve
le terrain beta.193 et nettoie la liste de création : Sanctuary et les types
vanilla, sans variantes de labo visibles. Le menu sans Amis sera examiné à la
sortie du solo. Aucune nouvelle retouche de relief ni série de captures décidée.
Le choix d'intégrer le profil de labo à la génération publique reste ouvert.
## Reprise du 1er octobre 2026 — interfaces et usages
**Décision du créateur :** arrêter les séries de screenshots et les retouches
de génération, biomes et chunks. Le laboratoire beta.193 reste le dernier
état de comparaison, avec ses réserves visuelles documentées ; cette pause
ne vaut pas validation esthétique complète. Les priorités worldgen ci-dessous
sont désormais historiques.
**Premier lot demandé : [UI-194](main-menu-beta194.md).** Retirer Amis du menu
principal, rapprocher Options et Quitter de Solo/Multijoueur et conserver la
navigation clavier. Ce retrait figurait déjà dans le cadrage Storyquest initial.
**Proposition de suite à discuter, sans lancement implicite :**
| Sujet déjà envisagé | Premier résultat utile |
| --- | --- |
| Habitant | Une page claire pour l’identité, la progression et le familier, à partir des données réellement disponibles |
| Combat et Factions | Clarifier les accès aux épreuves, aux membres et aux projets communs ; distinguer les fonctions présentes des projets futurs |
| Storyquest | Choisir une première découverte reliée au SGA et à une activité existante, puis afficher sa piste et sa conséquence personnelle ou collective |
| Familiers et cuisine | Revoir l’utilité d’un passif existant selon le port sur la tête et choisir un goût à découvrir ; leur lien avec la préparation d’une sortie reste à concevoir |
Le prochain travail peut utiliser l’île et ses lieux actuels. Les expansions,
la nouvelle hydrologie et de nouvelles structures ne sont pas des dépendances
de cette reprise des menus. Aucun passif, nouvelle quête ou écran supplémentaire
n’est annoncé comme implémenté par UI-194.
## Historique du chantier terrain
**Après la visite beta.190 :** le créateur demande de revenir au relief d’avant
les plateaux et de retirer aussi leurs affleurements. [WG-RESTORE-191](restore-relief-beta191.md)
restaure la base beta.188 ; cavités, creux et pointes restent la direction,
avec des formations singulières ponctuelles plutôt qu’un traitement uniforme.
**Après la visite beta.189, 30 septembre :** [WG-NATURE-190](natural-refinement-beta190.md)
reprend le relief naturel et les cascades. Ruines et salle inférieure appréciées ;
le choix de la ressource renouvelable d’activation du portail reste ouvert.
Les terrasses ciselées sont réservées à une expansion future dans le backlog.
La rosace actuelle est à Y=640 et le palais supérieur à Y=1278 ; les paragraphes
antérieurs ci-dessous conservent l’historique de conception.
**Après la visite beta.181 :** palette naturelle validée ; dégager les huit
axes. Le [lot ORIGIN-182](origin-gallery-beta182.md) prépare la galerie
souterraine et la variante aérienne en rosace. L’origine reste à Y=320.
**Actualisation du 30 septembre :** relief et donjon beta.180 appréciés en
visite. Le [lot ORIGIN-181](origin-landmark-beta181.md) fixe le bloc originel
en **(0,320,0)** dans un petit repère aérien ouvert. Il conserve les demandes
@@ -211,7 +264,7 @@ en réserve. L'essai peut conduire à simplifier, déplacer ou abandonner une id
La lecture minimale d'une demande ou d'une découverte accompagne sa scène dans
les interfaces existantes. La refonte complète Habitant/Combat/Factions reste
dans la réserve ; elle se précisera avec les gestes réellement testés. Le
retrait demandé de Friends reste une petite tâche explicite à réaliser, sans
retrait demandé de Friends est traité dans [UI-194](main-menu-beta194.md), sans
en faire une dépendance de la carte de l'île.
Les navets, la loterie, le shop, l'ensemble des créatures, le chenil, les
+100
View File
@@ -0,0 +1,100 @@
# FINISH-189 — refuges au sol, berges et repères souterrains
Ticket du 30 septembre 2026, branche `codex/terrain-finish-beta189`.
Retour de visite beta.188 à partir des captures 19:55:33 et 19:57:00.
## Contrat
Nouveau preset `sanctuary_test:terrain_finish_v1`, profil `finish`, graine 42.
La partie beta.188 reste ouverte et préservée. Aucune migration ou régénération.
Corrections demandées : refuges qui suivent le sol, sédiments suivant la profondeur,
sable au-delà des berges, rejet des arbres dès leur plantation, reliefs étagés et
roche apparente. Accent sur le portail inférieur par le sol et la lumière.
Essais bornés : déversoirs vers une ouverture proche si le terrain l'autorise ;
petites ruines dans un volume de grotte sec déjà présent, sans excavation.
Pas de nouveau biome de glace. Une autre variante de lush cave reste à concevoir.
## Réalisation
Le relief n'est transformé qu'entre Y=252 et Y=316 par une fonction analytique,
sans nouveau bruit ni nouvelle passe d'hydrologie. Les grottes profondes et récifs
aériens gardent leur densité. Les affleurements suivent escarpements et épaules.
Les refuges de surface choisissent leur sol dans la densité, sans feuilles.
Les fonds passent du sable peu profond à l'argile des zones calmes, avec gravier
sur les ruptures de pente. Berges sableuses sur trois blocs au maximum et jusqu'à
deux blocs au-dessus de l'eau. Les plantations natives de type arbre et système
racinaire sont rejetées avant toute écriture près des étangs.
Le portail horizontal 5×5 avec vide 3×3 reste en place. Sol taillé et éclairage
en cuivre/pierres lumineuses soulignent le centre et les accès ; chandelier
suspendu au toit existant. Les petites ruines sont un prototype, pas une ville
avec habitants, commerce ou nouvelles quêtes.
## Vérifications
Les résultats finaux et leurs limites sont détaillés ci-dessous.
## Résultat vérifié — graine 42
- Huit ancres aux mêmes emplacements et orientations qu'en beta.188, offres
vérifiées et remises à leur état initial après le contrôle. Les surfaces des
refuges suivent la densité rocheuse, sans consulter la hauteur des feuilles.
- 1 120 blocs de sable sur les berges sèches ; fonds de 1 146 blocs de sable,
376 de gravier et 2 896 d'argile ; 408 plantations aquatiques. Zéro tronc dans
les colonnes des étangs et des plages contrôlées, de 28 blocs sous l'eau à
28 blocs au-dessus. Les arbres voisins peuvent conserver un surplomb de feuilles.
- Une cascade : départ (143,244,-147), déversoir (153,244,-147). Chenal de dix
blocs, entaille locale de treize blocs au total. Soixante-quatre blocs de chute
sont préremplis ; les fluides natifs poursuivent la chute vers le vide.
- Quatre ruines de cinq blocs de côté, aux sols (24,163,-64), (-8,147,-80),
(64,163,-80), (56,120,-80). Appuis rocheux contrôlés, zéro bloc de cavité
excavé. C'est un petit ensemble de vestiges étagés, pas une ville complète
ni un nouveau réseau de couloirs.
- Portail conservé en (0,175,0), bordure 5×5 et ouverture sèche 3×3. Sol de
pierre taillée, accès lumineux et chandelier : vingt sources lumineuses
supplémentaires dans ce plan, sans creuser de nouvelle salle.
- Sur le relevé du relief : 616 hauteurs de surface modifiées, quatre blocs
d'écart maximum ; 337 points d'affleurement. Les 25 205 échantillons de densité
des profondeurs et des récifs restent identiques. La compression conserve les
fines crêtes au lieu de les supprimer entre deux échantillons.
- Palais, 49 cartes verrouillées dans leurs 49 cadres horizontaux, gemmes,
soufre, butin en fer enchanté et passages du donjon contrôlés. Cartes et état
initial des ancres relus dans la copie destinée au solo.
Génération puis réouverture réussies : `build/finish189g-cold.json` et
`build/finish189g-warm.json`. Démarrage du serveur : 11,369 s / 0,581 s ; processus
prêt après l'ensemble des contrôles : 67,717 s / 19,973 s. Zéro région d'hydrologie.
Plans locaux des berges/déversoirs : 137 ms ; ruines : 459 ms à froid. Ce sont des
mesures de laboratoire, pas une garantie de durée identique sur chaque machine.
Le serveur de diagnostic désactive son watchdog : la vérification charge les
zones et remplit toutes les cartes dans un seul appel. Un premier contrôle a
atteint les 60 s pendant l'atlas. Le solo garde le remplissage progressif habituel
et ne lance pas cette batterie de mesures au démarrage.
`./gradlew check build assemblePack assembleTestPack` réussi en 9 min 1 s,
avec 265 GameTests réussis. Après les derniers correctifs du module de labo,
compilation/JAR ciblés, génération et réouverture natives réussies ; pack
optionnel resynchronisé. Ses 165 classes correspondent aux classes compilées
et le JAR distribué est identique : `build/finish189-pack-receipt.json`.
Aucune publication de canal, installation Prism ou ancienne sauvegarde modifiée.
## Visite
Nouvelle sauvegarde `Sanctuary-Terrain-Finish-189-Solo`, profil `finish`, run
`visite189`, graine 42. Vue 32, simulation 12, spectateur, commandes activées.
Solo Vulkan ouvert : connexion confirmée à 20:29:55, distances 32/12 et
masquage du palais depuis le sol confirmés dans `build/finish189-solo.log`.
- Étang ouest : `/tp -104.5 269 119.5 180 35`
- Ancre nord : `/tp 36.5 252 -168.5 180 0`
- Cascade : `/tp 158.5 248 -146.5 90 25`
- Portail inférieur : `/tp 0.5 179 10.5 180 20`
- Ruines : `/tp 24.5 167 -71.5 0 20`
Le jugement esthétique reste à faire pendant la nouvelle visite. La glace et
un biome supplémentaire ne sont pas ajoutés. Piste proposée pour plus tard :
une cavité claire de calcite et d'améthyste, plus calme que les jardins lush,
les marais et le soufre ; aucune décision d'implémentation à ce stade.
+4
View File
@@ -1,5 +1,9 @@
# UI-045 — menu principal
Contrat historique : [UI-194](main-menu-beta194.md) retire ensuite la ligne
Friends / Amis et rapproche les commandes restantes. Les essais ci-dessous
décrivent beta.045, pas le menu courant.
Branche `codex/title-menu-beta045`, à partir de beta.044.
## Contrat
+7
View File
@@ -1,5 +1,12 @@
# Sanctuary — vision du projet
**Cap du 1er octobre 2026 :** le créateur suspend les retouches du worldgen,
des biomes et des chunks ainsi que les tests de screenshots après beta.193.
La reprise porte sur les interfaces et les idées du Storyquest, avec le retrait
d’Amis du menu principal en premier. Voir le [fil rouge courant](storyquest-fil-rouge.md)
et [UI-194](main-menu-beta194.md). Les priorités de septembre ci-dessous
conservent l’historique ; elles ne relancent pas le chantier terrain.
**Direction courante — 29 septembre 2026 :** le
[fil rouge de la refonte](storyquest-fil-rouge.md) reprend **Sanctuary Island,
l'île de départ**, autour de plans géographiques indépendants et de parcours
+167
View File
@@ -0,0 +1,167 @@
# WG-CONTINUITY-193 — raccordements et visites sur plusieurs graines
Branche `codex/worldgen-continuity-beta193`, Minecraft 26.3.
Nouveau profil de laboratoire `coherent`, mondes neufs uniquement.
Graines vérifiées : 42 (référence des retours), 2026 et 8675309.
## Demande
Conserver la direction des montagnes appréciée en beta.192 et corriger les
raccordements d’eau, le damier de mousse des ancres et les longues marches.
Vérifier plusieurs graines et examiner des captures réelles du jeu avant de
préparer la prochaine visite. La quantité globale de roche n’est pas à réduire
systématiquement ; la réserve précédente du créateur demeure.
## Contrat de cette itération
- Propriété commune des espaces remplis d’eau : éviter les nappes indépendantes
qui se recoupent à des hauteurs différentes dans une même cavité.
- Ancres de surface et aériennes adaptées à la densité du terrain, sans fond
ondulé artificiel ni damier de mousse ; conserver secteurs et orientation.
- Éviter les sols de grotte en plein air et interrompre localement les longues
marches du massif sans remettre des niveaux périodiques sur toute l’île.
- Ajouter des contrôles de raccordement, pas seulement de nombre de blocs.
- Produire des captures Vulkan de bassins, d’ancres et du relief dans des mondes
de diagnostic neufs, avec graine, position et version identifiées.
Aucune modification, régénération ou migration des sauvegardes précédentes.
Le profil beta.192 reste disponible avec son comportement antérieur.
## Changements de génération
Les volumes des étangs de surface évitent désormais les volumes déjà retenus
dans les cavités. L’eau est posée avant la décoration et ses colonnes sont
protégées contre les arbres et systèmes de racines. Les contrôles comptent
chaque cellule humide prévue, y compris celles que l’ancien contrôle ignorait
quand un autre bloc avait remplacé l’eau. Les berges sableuses débordent moins
haut et moins loin sur les flancs raides.
Les ancres aériennes utilisent le même relevé de sol que les ancres de surface.
Le chemin suit les colonnes réelles ; son admission refuse les sauts de hauteur.
L’herbe reprend la couleur du biome et la terre remplace le damier de mousse.
Les grottes gardent leurs palettes ; au-dessus de Y=240, une couverture rocheuse
locale doit réellement exister pour attribuer une palette de grotte.
Le massif 192 reste la base. De petites entailles irrégulières interrompent ses
contours, sans paliers périodiques ni modification des densités profondes et
aériennes. Les cerisiers cherchent un emplacement sec et dégagé près des
sommets choisis ; leur altitude suit le relief de la graine.
Les essais ont aussi révélé des collisions anciennes : la galerie choisit ses
salles secondaires dans des cavités voisines séparées de la pièce centrale.
Elle conserve seulement les salles admises, avec leurs liaisons praticables.
Les bassins évitent cette galerie et peuvent être moins nombreux si l’espace
manque. Les ruines évitent son volume et les geysers évitent les ruines.
Ces recherches restent bornées et leurs plans sont mis en cache ; aucun solveur
d’hydrologie régionale ni simulation permanente n’est ajouté.
## Méthode de comparaison
Le script `scripts/worldgen_lab.py verify --profile coherent --seed 42 --run NOM`
crée un laboratoire neuf, vérifie les blocs natifs, le ferme puis le rouvre.
Répéter avec 2026 et 8675309 ; les journaux et JSON restent dans leurs dossiers.
Une modification du code ou des données impose un nouveau nom d’expérience.
`scripts/worldgen_photos.py` prépare ensuite une copie neuve du monde vérifié,
un plan de caméras et un fichier de provenance. Il requiert `nbtlib`, `--source-run`,
`--seed`, `--run` et `--previous-client` (dossier client d’un ancien labo fermé).
La commande `worldgen_lab.py solo --profile coherent --seed GRAINE --run PHOTOS
--photo-plan CHEMIN/cameras.json` prend les images avec le moteur Vulkan du jeu.
Le mode photo refuse les sauvegardes qui ne portent pas le préfixe réservé
`Sanctuary-Photo-193-`. Il masque l’interface, attend le rendu des sections,
conserve une heure de midi fixe, capture puis ferme proprement le client.
Sans cette option, le solo reste une visite manuelle normale.
Les positions viennent des aménagements réellement générés : chaque bassin,
deux côtés du massif, les ancres aériennes et une ancre de surface. Une capture
réussie doit être examinée visuellement ; un nombre de blocs correct ne valide
ni la beauté des rives ni la silhouette du relief.
## Résultats
Génération et réouverture natives réussies pour les trois graines dans les
runs `coherent193k42`, `coherent193k2026` et `coherent193k8675309`.
Les plans d’eau, emplacements des ancres et relevés de relief sont identiques
après rechargement (`build/coherent193-three-seeds.json`).
| Graine | Volumes d’eau contrôlés | Cellules humides | Salles de galerie | Coffres de ruines |
| --- | ---: | ---: | ---: | ---: |
| 42 | 5 | 28212 | 3 | 4 |
| 2026 | 5 | 21542 | 3 | 3 |
| 8675309 | 4 | 8929 | 2 | 4 |
Sur chaque graine : zéro contact entre volumes indépendants, toutes les cellules
prévues encore humides, huit ancres contrôlées en blocs réels, aucun damier de
mousse dans leurs plans exposés, trois cerisiers réellement présents et 39 366
échantillons profonds/aériens inchangés. Le donjon, les gemmes, la galerie, le
soufre, les ruines, la rosace et les 49 cartes/cadres de l’ISS passent aussi les
contrôles natifs. La graine 42 admet un déversoir large de cinq colonnes ; les
deux autres gardent leurs bassins fermés quand aucun déversoir proche n’est admis.
Démarrages à froid : 15,2 / 18,9 / 19,1 s ; réouvertures : 0,6 / 0,7 / 1,0 s.
Zéro région d’hydrologie lourde. Avec l’ensemble des diagnostics et de l’atlas,
les essais à froid durent 112 / 188 / 106 s. Les processus tournaient en partie
en parallèle ; ce ne sont pas des benchmarks isolés et le solo ne rejoue pas
ces contrôles exhaustifs à chaque ouverture.
`./gradlew check build assemblePack assembleTestPack` réussi, 265 GameTests.
Le premier passage avait échoué sur le déplacement d’un familier volant, sans
modification de ce système ; la relance a passé les 265 tests. Après les derniers
ajustements du laboratoire, `:sanctuary-test:check :sanctuary-test:build` et le
réassemblage du pack de test ont réussi. 179 classes et 124 ressources du JAR
sont identiques aux sorties courantes et le JAR distribué correspond octet pour
octet (`build/coherent193-pack-receipt.json`). Pas de publication ni de mise à
jour de Prism.
## Examen des captures
La série retenue rassemble 29 PNG natifs 2560 × 1440 : 10 pour 42 dans
`photos193d`, 10 pour 2026 dans `photos193c` et 9 pour 8675309 dans `photos193e`.
Les dossiers sont sous `build/worldgen-lab/RUN/coherent/GRAINE/` ; les positions,
l’empreinte des sources et les empreintes des PNG sont conservées dans
`photo-provenance.json`. Galerie locale : `build/worldgen193-gallery/index.html`.
Les premières images nocturnes de 42 et pluvieuses de 8675309 sont conservées
comme diagnostics, hors de la galerie finale. Les services de temps réel et de météo quotidienne sont désactivés dans les
seuls clients photo jetables ; le script prépare également une longue période
de temps clair natif. La météo quotidienne avait rétabli la pluie après un
premier essai avec seulement les durées natives modifiées. L’automate ne remplit l’identité
« Labo » et ne passe l’avertissement expérimental que dans ces copies réservées.
Constats de l’assistant, sans validation esthétique du créateur :
- Les nappes retenues sont séparées et l’eau reste présente après décoration.
Cela ne suffit pas à rendre les rives naturelles : 42 conserve notamment une
longue retenue sableuse contre le massif ; les liserés restent trop réguliers.
- Les abords des ancres aériennes suivent le sol, sans l’ancien tapis de mousse
surélevé. Le motif alterné terre/herbe et pierre/herbe reste très quadrillé,
particulièrement sur l’ancre automnale de 42 et celle de surface de 2026.
- Certains fonds de bassins de cavité restent herbeux. Leur sédimentation n’est
pas harmonisée avec celle des étangs de surface dans cette itération.
- Les trois massifs diffèrent, mais de grandes bandes et marches rocheuses
persistent, surtout sur 42 et 2026. Les petites entailles n’ont pas suffi à
résoudre cette réserve esthétique. Aucune disparition générale revendiquée.
- Une vue de l’ancre de surface de 42 est masquée par le feuillage ; les autres
points de vue et contrôles de blocs ne remplacent pas sa visite rapprochée.
De larges ombres sont aussi visibles sur 42 : leur cause n’est pas établie.
Pour la prochaine retouche, comparer les mêmes caméras, une modification à la
fois, en commençant par la retenue sur le massif et les chemins quadrillés.
Les trois graines constituent une régression reproductible, pas une preuve
pour toutes les graines ou une validation esthétique de toute la carte.
## Visite manuelle livrée
`build/worldgen-lab/visite193/coherent/42/client/saves/` contient trois copies
neuves : `Sanctuary-Coherent-193-42-Solo`, `Sanctuary-Coherent-193-2026-Solo`
et `Sanctuary-Coherent-193-8675309-Solo`. Elles proviennent des mondes photo
fermés, dont les huit ancres sont inactives et le Bugrock allumé à zéro relais.
Le client manuel est entré sur 42 le 1er octobre 2026 à 01:26:55 Europe/Paris,
vue 32 chunks, simulation 12, joueur spectateur placé devant le massif.
L’automate photo n’est pas activé. Les deux autres mondes sont disponibles
par la liste Solo ; ils n’ont pas été revisités manuellement après leur copie.
Les services d’heure et météo réelle restent désactivés dans ce labo de
comparaison ; midi et temps clair sont préparés dans ces copies seulement.
Quelques avertissements de retard de ticks apparaissent pendant le chargement
à 32 chunks : aucune mesure de fluidité stable n’est revendiquée.
+89
View File
@@ -0,0 +1,89 @@
# WG-FIX-197 — arrivée, cascades et détails du donjon
Branche `codex/worldgen-fixes-beta197`, depuis beta.196. Retours R012 :
arrivée sur l’ISS, cascades souterraines, diagonales de terre dans la mine,
toit de cabane cassé, arrêt chez un ami pendant la création Large sans rapport.
Graine de cet arrêt inconnue. Minecraft 26.3, Fabric 0.19.5, Java 25.
## Changements
Les nouveaux profils `shared_island197_{small,medium,large}` conservent les
densités et biomes du terrain beta.196. Le paramètre additif `polish197`, faux
par défaut, active uniquement les corrections de décoration. Les identifiants
beta.196, données et comportements historiques restent présents. Personnaliser
un profil historique conserve sa révision. Aucune migration ni retouche de
sauvegarde personnelle.
La hauteur du point global ne suffisait pas : `PlayerSpawnFinder` recommence
une recherche depuis la surface la plus haute de chaque colonne. Le toit de
l’ISS pouvait donc être retenu. Sur les nouveaux profils, cette recherche
reste bornée au terrain principal (Y=198–316), avec sol solide, deux blocs
d’air et exclusion des feuilles et troncs. Le rayon aléatoire natif et les
réapparitions personnelles sur un lit restent gérés par Minecraft.
Une poche d’air sous une berge ne suffit plus pour accepter une cascade.
Chaque sortie doit être ouverte au ciel et posséder une face extérieure libre
sur les huit premiers blocs de chute. Les recherches conservent leurs bornes :
dix blocs jusqu’à la sortie, au plus deux bouches de quatre ou cinq blocs.
Une cascade n’est pas forcée si le terrain ne s’y prête pas. Les blocs de chute
ne remplacent jamais un bloc solide réel ; une corniche peut arrêter l’eau.
Le sol des salles minières remplace le modulo 7 par des plaques de terre
cohérentes, dépendantes de la graine. La cabane utilise deux pans d’escaliers
continus, un faîtage, des pignons fermés et des pilotis rejoignant un support.
Son volume est vérifié avant placement et réservé contre les décorations
natives voisines. Aucun grand terrassement ajouté pour imposer une cabane.
## Erreur de création reproduite
La graine **17 en Medium** a ensuite reproduit une exception réelle pendant
la génération du premier chunk : `No supported central gallery floor`, dans
`Cavern183.create`. Aucune cavité admissible n’existait exactement sous (0,0).
Sur beta.197, une recherche locale de roche porteuse sert désormais de repli
pour sculpter la même chambre centrale ; elle ne déplace pas le portail ni les
salles déjà admises sur les autres graines. Création et réouverture de cette
graine réussies en Medium et Large. Ce défaut identifié ne prouve pas encore
la cause de l’arrêt sans rapport chez l’ami. `latest.log` reste utile ; une
ligne indique maintenant taille, graine et plafond mémoire au démarrage.
## Vérifications
- Témoin beta.196 Large, graine 42 : création et réouverture réussies.
- beta.197 : Small 0, Large 42, Medium 17 et Large 17 créés puis rouverts avec
huit ancres, donjon, gemmes, atlas, portail et contrôles d’eau. Les quatre
cabanes ont leurs 99 blocs de toiture intacts, avant et après réouverture.
- Dans ces quatre essais, aucune bouche de cascade extérieure admissible :
les anciennes chutes enfouies sont rejetées. Le contrôle reste conditionnel,
il ne constitue pas une validation visuelle d’une nouvelle cascade acceptée.
- Lecture des blocs sauvegardés des salles Large 42 : ancien motif, 75 blocs
de terre tous isolés ; nouveau sol, 160 blocs dont un seul isolé. La terre
forme donc des plaques au lieu de diagonales pointillées.
- Parcours client natif Vulkan réussi, vue 8 chunks, mémoire Java 2 Gio :
menus FR/EN, trois tailles, Annuler/Échap, aller-retour disque, ancien profil
conservé, types vanilla. Création Large 0 puis arrivée réelle à Y=252 et
huit recherches de spawn aléatoires restées sur l’île. Pas de campagne de
captures, ni de revendication de fluidité à 32 chunks sur le matériel ami.
Les essais Small 0/Large 42 et client ont précédé le dernier repli de galerie,
qui ne s’exécute que lorsqu’aucun des anciens sols n’a été trouvé. Les scénarios
17 vérifient ce changement final. Une première tentative du test client a été
arrêtée pendant l’attente inutile de tous les chunks de la vue ; une fixture
de menu historique a ensuite été corrigée pour conserver la clé publique.
Seul le passage final réussi est retenu.
Traces ignorées : `build/faults197-client-verified.log`,
`build/worldgen-lab/faults197a/{small/0,large/42}/{cold,warm}.json`,
`build/worldgen-lab/faults197b/{medium,large}/17/{cold,warm}.json`.
La panne initiale est conservée dans `faults197a/medium/17/cold.log`.
`./gradlew check build assemblePack assembleTestPack -PsanctuaryFocusedTests=menus`
réussi en 5 min 18 s : contrôles purs, quatre GameTests menus et assemblages.
Export ZIP, versions, dépendances, six presets anciens/nouveaux et toutes les
classes des deux JAR comparés aux classes construites. La dépendance Fabric API
conserve exactement le téléchargement et les hashes vérifiés en beta.196.
Export local `Downloads/Sanctuary-Test-beta.197.mrpack`, **12 276 176 octets**.
SHA-256 : `90b0e0495c7fbdee5ceb1b3c87f177274d89d505a6eea7189358aab3c7fff5b2`.
Guide séparé ; aucun monde, réglage personnel, journal ou secret dans l’archive.
Ni publication du canal ni installation dans Prism. Reçu :
`build/faults197-artifact.json` ; journal général : `build/faults197-check-build.log`.
+79
View File
@@ -0,0 +1,79 @@
# WG-OPTIONS-195 — Sanctuary et les types vanilla
Branche `codex/worldgen-options-beta195`, Minecraft 26.3, depuis beta.194.
## Demande et périmètre
Le créateur reprend la visite de Sanctuary Island pour décider si la génération
peut rester en pause. Il demande une nouvelle île avec la graine 0 et souhaite
examiner le menu principal en quittant le solo. Il précise que la création de
monde doit conserver Sanctuary et les types vanilla, sans les variantes de labo.
Le module Sanctuary Test cesse d'ajouter ses onze anciens presets au tag de
sélection normal. Les types vanilla et `sanctuary:sanctuary` restent disponibles.
Les presets et leurs codecs sont conservés pour les sauvegardes et les lanceurs
de développement ; aucune suppression, migration ou régénération de monde.
Les sélections historiques des GameTests restent dans leurs seules ressources
de test, avec références optionnelles au module Sanctuary Test.
Ce nettoyage ne remplace pas la génération publique Sanctuary par le profil
de laboratoire : leur consolidation reste à décider après la visite. Les options
de taille de Sanctuary sont conservées. Les traductions et identifiants existants
restent identiques ; aucun nouveau libellé d'interface.
## Correction révélée par la graine 0
Le premier démarrage a échoué : `No supported central gallery floor`. La
recherche historique ne sondait que Y=148–212 au centre. En cas d’échec dans le
profil `coherent` seulement, une seconde recherche bornée examine Y=48–240,
avec les mêmes exigences de sol rocheux et de couverture. Les sols déjà admis
sont conservés. Ce correctif concerne le placement de la galerie, pas la forme
de l’île. Le monde avorté est conservé ; le nouvel essai utilise un autre dossier.
## Visite préparée
Nouveau dossier ignoré `build/worldgen-lab/visite195d/coherent/0/`, profil
`sanctuary_test:coherent_landforms_v1`, graine 0, diamètre 724. Le terrain et
les aménagements sont ceux de beta.193, avec le repli de placement décrit
ci-dessus ; beta.194 fournit le menu sans Amis.
Solo spectateur, commandes autorisées, distance de rendu 32, simulation 12,
Vulkan, journée claire pour comparer le terrain. Aucune série de screenshots.
## Vérifications
`./gradlew check build assemblePack assembleTestPack
:sanctuary-test:exportDuoLaunch -PsanctuaryFocusedTests=menus` réussi en 3 min 45 s,
avec les quatre GameTests menus. Après le correctif de galerie, reconstruction
et contrôle du module de laboratoire réussis. Une passe générale supplémentaire
qui avait aussi lancé les tests de capacité est interrompue pour libérer la
machine lors de la visite : elle ne constitue pas un résultat de 265 tests.
Le réassemblage final réussit en 4 s en reprenant les vérifications déjà
réussies, sans les relancer.
Création native de la graine 0 réussie dans `visite195d` : démarrage serveur
14,8 s, contrôle des lieux terminé à 108,1 s, zéro région d’hydrologie lourde.
Trois salles reliées, portail horizontal en (0,131,0), huit ancres avec huit
matériaux distincts, cinq volumes d’eau séparés et 20 033 cellules humides
contrôlées. Les 49 cartes/cadres du palais et les autres contrôles de donjon,
soufre, ruines et gemmes passent. Ces durées incluent des diagnostics et une
construction concurrente ; elles ne sont pas un benchmark de fluidité.
Une copie neuve `Sanctuary-Labo-195-Seed-0` est préparée pour la visite, avec le
visiteur de développement KokaLab déjà inscrit et les ancres dans leur état
initial (le contrôle des offres restaure ses modifications). La graine 0 est
relue dans les données enregistrées. Le client beta.195 a démarré sous Vulkan. Après l’avertissement expérimental,
les journaux confirment KokaLab connecté le 1er octobre à 18:00:35 Europe/Paris,
en (100.5,335,150.5), rendu 32 chunks et simulation 12. Le contrôle natif de
visibilité de la couronne passe. L’examen esthétique et celui du menu principal
sont laissés au créateur.
Journaux : `build/worldgen195-check-build.log`, `build/worldgen195-floor-build.log`,
`build/worldgen195-final-pack.log` (passe interrompue),
`build/worldgen195-assemble.log`, `build/visit195-solo.log` et le rapport natif
`build/worldgen-lab/visite195d/coherent/0/server/worldgen-lab-metrics.json`.
Le reçu `build/worldgen195-artifact.json` confirme que les classes du mod principal
sont identiques à beta.194. Dans le mod de laboratoire, seul `Cavern183.class`
change ; les 22 presets et toutes les ressources du terrain restent identiques,
hormis le tag de sélection. Les anciennes sauvegardes n’ont pas été ouvertes.
Aucune capture ni validation esthétique de cette nouvelle graine revendiquée.
Pas de publication du canal packwiz ni de mise à jour de Prism.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.181
pack_version=beta.181
mod_version=beta.203
pack_version=beta.203
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+6
View File
@@ -51,4 +51,10 @@ tasks.register('exportDuoLaunch') {
}
}
tasks.named('test') { failOnNoDiscoveredTests=false }
tasks.register('gallery198Smoke', JavaExec) {
dependsOn('testClasses')
classpath=sourceSets.test.runtimeClasspath
mainClass='fr.koka.sanctuarytest.GalleryFloor198Test'
}
tasks.named('check') { dependsOn('gallery198Smoke') }
tasks.named('jar') { from(rootProject.file('LICENSE')) { rename { 'LICENSE_sanctuary_test' } } }
@@ -12,18 +12,40 @@ import net.minecraft.world.level.biome.*;
public final class AdventureBiomes180 extends BiomeSource {
public static final int AUTUMN_MIN=200, AUTUMN_MAX=232;
public static final MapCodec<AdventureBiomes180> CODEC=RecordCodecBuilder.mapCodec(i->i.group(
Biome.CODEC.listOf().fieldOf("biomes").forGetter(s->s.biomes)
Biome.CODEC.listOf().fieldOf("biomes").forGetter(s->s.biomes),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("discovery",false).forGetter(s->s.discovery),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("natural_sulfur",false).forGetter(s->s.naturalSulfur),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("original_relief",false).forGetter(s->s.originalRelief),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("polish197",false).forGetter(s->s.polish197),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("stable_gallery198",false).forGetter(s->s.stableGallery198)
).apply(i,AdventureBiomes180::new));
public record Terrain(int top,boolean cliff,boolean valley,int lowestNeighbour,int higherNeighbours){}
private record State(long seed,ReliefEcology177.Field field,Map<Long,Terrain> terrain){}
private final List<Holder<Biome>> biomes;
private final boolean discovery,naturalSulfur,originalRelief,polish197,stableGallery198;
private final Map<String,Holder<Biome>> named;
private volatile State state;
volatile LabExpansions199 expansions;
private static final int[][] DIRECTIONS={{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
public AdventureBiomes180(List<Holder<Biome>> biomes){
public AdventureBiomes180(List<Holder<Biome>> biomes,boolean discovery){this(biomes,discovery,false);}
public AdventureBiomes180(List<Holder<Biome>> biomes,boolean discovery,boolean naturalSulfur){this(biomes,discovery,naturalSulfur,false);}
public AdventureBiomes180(List<Holder<Biome>> biomes,boolean discovery,boolean naturalSulfur,boolean originalRelief){
this(biomes,discovery,naturalSulfur,originalRelief,false);
}
public AdventureBiomes180(List<Holder<Biome>> biomes,boolean discovery,boolean naturalSulfur,boolean originalRelief,boolean polish197){
this(biomes,discovery,naturalSulfur,originalRelief,polish197,false);
}
public AdventureBiomes180(List<Holder<Biome>> biomes,boolean discovery,boolean naturalSulfur,boolean originalRelief,boolean polish197,boolean stableGallery198){
this.stableGallery198=stableGallery198;
this.polish197=polish197;
this.originalRelief=originalRelief;
this.naturalSulfur=naturalSulfur;
if(naturalSulfur&&biomes.stream().noneMatch(b->b.unwrapKey().orElseThrow().identifier().getPath().equals("natural_sulfur_187")))throw new IllegalArgumentException("Missing natural sulfur biome");
this.discovery=discovery;
this.biomes=List.copyOf(biomes);var names=new HashMap<String,Holder<Biome>>();
for(var b:biomes)names.put(b.unwrapKey().orElseThrow().identifier().getPath(),b);
named=Map.copyOf(names);
if(discovery&&!named.containsKey("discovery_sulfur"))throw new IllegalArgumentException("Missing discovery sulfur biome");
for(var n:List.of("forest","flowers","autumn","cherry","jungle","swamp","bare","cliffs","caves","grove","lush","marsh","mycelium","summit","void"))
if(!named.containsKey("adventure_"+n))throw new IllegalArgumentException("Missing rocky biome "+n);
}
@@ -34,7 +56,7 @@ public final class AdventureBiomes180 extends BiomeSource {
public Terrain terrain(int x,int z){
var s=state;if(s==null)throw new IllegalStateException("Unbound rocky ecology");
final int qx=x>>2<<2,qz=z>>2<<2;
if(Math.hypot(qx,qz)>398)return new Terrain(-1,true,false,-1,0);
if(Math.hypot(qx,qz)>s.field().base().terrainBound())return new Terrain(-1,true,false,-1,0);
return s.terrain().computeIfAbsent(((long)qx<<32)^(qz&0xffffffffL),ignored->{
int top=s.field().surface(qx,qz),lowest=320,higher=0,highest=-1;boolean steep=false;
for(var d:DIRECTIONS){
@@ -49,6 +71,12 @@ public final class AdventureBiomes180 extends BiomeSource {
return new Terrain(top,cliff,valley,lowest,higher);
});
}
/** Decoration policy only; the density function and biome selection stay identical to beta.188. */
public boolean stableGallery198(){return stableGallery198;}
public boolean polish197(){return polish197;}
public boolean originalRelief(){return originalRelief;}
public boolean naturalSulfur(){return naturalSulfur;}
public long seed(){return state.seed();}
public ReliefEcology177.Field field(){return state.field();}
public static double moisture(long seed,int x,int y,int z){
double h=y/48.0;int layer=(int)Math.floor(h);double t=h-layer;t=t*t*(3-2*t);
@@ -57,8 +85,10 @@ public final class AdventureBiomes180 extends BiomeSource {
return a+(b-a)*t;
}
public Holder<Biome> resolve(int x,int y,int z){
var extra=expansions;
if(extra!=null&&extra.regionAt(x,z)!=null)return extra.source().biomeAt(x,y,z);
var s=state;if(s==null)throw new IllegalStateException("Unbound rocky ecology");
if(y<0 || y>=512 || Math.hypot(x,z)>398)return get("void");
if(y<0 || y>=512 || Math.hypot(x,z)>s.field().base().terrainBound())return get("void");
if(y>=320){
int nearest=-1;double best=Double.POSITIVE_INFINITY;var reefs=s.field().base().reefs();
for(int i=0;i<reefs.size();i++){
@@ -67,8 +97,10 @@ public final class AdventureBiomes180 extends BiomeSource {
}
return get(switch(nearest){case 0->"cherry";case 1->"autumn";default->"bare";});
}
if(discovery&&!naturalSulfur&&x>=-184&&x<=-120&&z>=124&&z<=180&&Sulfur183.biome(this,x,y,z))return named.get("discovery_sulfur");
var t=terrain(x,z);if(t.top()<0)return get("void");
if(y<t.top()-12){
if(y<t.top()-12 && (!s.field().coherent()||y<240||Continuity193.covered(s.field(),x,y,z))){
if(naturalSulfur&&Sulfur187.biome(s.seed(),x,y,z,t.top()))return named.get("natural_sulfur_187");
double wet=moisture(s.seed(),x,y,z);
double kind=moisture(s.seed()^0x179B10L,x+71,y,z-93);
if(wet<-.30)return get("caves");
@@ -12,7 +12,20 @@ final class AdventureChecks180 {
static Map<String,Object> check(ServerLevel level){
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();
for(var p:Cherry180.sites(source)){level.getChunk(p.getX()>>4,p.getZ()>>4);}
var trees=Cherry180.trees(source);if(trees.size()!=3)throw new IllegalStateException("Expected three main-island cherry trees: "+trees);
var trees=Cherry180.trees(source);
if(trees.isEmpty()){
// On reload the placement counter is empty; inspect the saved trunks without regenerating them.
var saved=new ArrayList<BlockPos>();
int start=source.field().eroded()?0:5,end=source.field().eroded()?15:11;
for(var site:Cherry180.sites(source))for(int x=(site.getX()>>4<<4)+start;x<=(site.getX()>>4<<4)+end;x++)
for(int z=(site.getZ()>>4<<4)+start;z<=(site.getZ()>>4<<4)+end;z++)for(int y=source.field().coherent()?251:271;y<=316;y++){
var p=new BlockPos(x,y,z);
if(level.getBlockState(p).is(Blocks.CHERRY_LOG)&&AdventureCaves180.natural(level.getBlockState(p.below())))saved.add(p);
}
trees=List.copyOf(saved);
}
int expected=source.field().coherent()?Cherry180.sites(source).size():3;
if(expected<2||expected>3||trees.size()!=expected)throw new IllegalStateException("Expected "+expected+" main-island cherry trees: "+trees);
for(var p:trees)if(!level.getBlockState(p).is(Blocks.CHERRY_LOG))throw new IllegalStateException("Missing actual cherry trunk at "+p);
var reefs=source.field().base().reefs();
if(!AdventureCaves180.biome(source,reefs.get(0).x(),reefs.get(0).y(),reefs.get(0).z()).equals("adventure_cherry")||!AdventureCaves180.biome(source,reefs.get(1).x(),reefs.get(1).y(),reefs.get(1).z()).equals("adventure_autumn"))throw new IllegalStateException("Wrong themed reefs");
@@ -8,8 +8,10 @@ import net.minecraft.world.level.levelgen.material.MaterialRuleContext;
import net.minecraft.world.level.levelgen.material.rule.*;
/** Expose the actual geological beds; moss is a thin local patch, never a universal cave soil. */
public record AdventureStone180() implements MaterialRule {
public static final MapCodec<AdventureStone180> CODEC=MapCodec.unit(AdventureStone180::new);
public record AdventureStone180(java.util.Optional<net.minecraft.world.level.levelgen.densityfunction.DensityFunction> slope) implements MaterialRule {
public static final MapCodec<AdventureStone180> CODEC=com.mojang.serialization.codecs.RecordCodecBuilder.mapCodec(i->i.group(
net.minecraft.world.level.levelgen.densityfunction.DensityFunction.CODEC.optionalFieldOf("slope").forGetter(AdventureStone180::slope)
).apply(i,AdventureStone180::new));
private static final java.util.Set<String> CAVES=java.util.Set.of("adventure_caves","adventure_grove","adventure_lush","adventure_marsh","adventure_mycelium");
private static double volume(long seed,int x,int y,int z,double size){
int layer=(int)Math.floor(y/size);double v=y/size-layer;v=v*v*(3-2*v);
@@ -18,12 +20,20 @@ public record AdventureStone180() implements MaterialRule {
}
@Override public RuleEvaluator compile(MaterialRuleContext context){
long seed=context.getOrCreateRandomFactory(Identifier.fromNamespaceAndPath("sanctuary_test","ecology_strata_v1")).at(0,0,0).nextLong();
var slopeGetter=slope.map(d->context.getDensitiesInChunk(d,false)).orElse(null);
return new RuleEvaluator(){
int lastX=Integer.MIN_VALUE,lastZ=Integer.MIN_VALUE;double warp;
@Override public BlockState tryApply(int x,int y,int z){
if(x!=lastX || z!=lastZ){lastX=x;lastZ=z;warp=EcologyStrata176.warp(seed,x,z);}
String biome=context.getBiome().unwrapKey().orElseThrow().identifier().getPath();
boolean cave=CAVES.contains(biome);
boolean sulfur=biome.equals("natural_sulfur_187");
boolean cave=sulfur||CAVES.contains(biome);
if(sulfur){
// Coherent mineral beds on the original rock, including ceilings. Never carve a shell.
double deposit=volume(seed^0x1875A1L,x,y,z,17);
if(deposit>-.08)return (deposit>.51?Blocks.CINNABAR:Blocks.SULFUR).defaultBlockState();
if(deposit>-.25)return Blocks.CALCITE.defaultBlockState();
}
if(y>=48 && context.stoneDepthAbove()<=3){
if(cave){
double patch=volume(seed^0x178A055L,x,y,z,8);
@@ -34,7 +44,8 @@ public record AdventureStone180() implements MaterialRule {
if(biome.equals("adventure_grove"))return (patch>.10?Blocks.MOSS_BLOCK:patch<-.35?Blocks.PODZOL:Blocks.GRASS_BLOCK).defaultBlockState();
if(patch>.72)return Blocks.MOSS_BLOCK.defaultBlockState();
}
}else if(!biome.equals("adventure_cliffs") && !biome.equals("adventure_void"))
}else if(!biome.equals("adventure_cliffs") && !biome.equals("adventure_void")
&& !(slopeGetter!=null && y>=248 && y<320 && slopeGetter.get()>.95f))
return (context.stoneDepthAbove()==1?Blocks.GRASS_BLOCK:Blocks.DIRT).defaultBlockState();
}
double depth=y+warp;var bed=EcologyStrata176.rock(depth,y);
@@ -0,0 +1,75 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.*;
import java.util.*;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.*;
import net.minecraft.world.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Block;
/** Local offers for this new preset only. Native block states own progress; no expansion is activated. */
final class AnchorOffers186 {
static final BlockPos CORE=new BlockPos(0,640,0);
static final List<PalaceAnchors.Demand> DEMANDS=List.of(
new PalaceAnchors.Demand(Items.STONE,32),new PalaceAnchors.Demand(Items.OAK_LOG,16),
new PalaceAnchors.Demand(Items.WHEAT,16),new PalaceAnchors.Demand(Items.COPPER_INGOT,16),
new PalaceAnchors.Demand(Items.COAL,16),new PalaceAnchors.Demand(Items.AMETHYST_SHARD,8),
new PalaceAnchors.Demand(Items.IRON_INGOT,8),new PalaceAnchors.Demand(Items.GLASS,16));
private AnchorOffers186() {}
static void register(){
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
if(!(player instanceof ServerPlayer p)||!(world instanceof ServerLevel level)
||hand!=InteractionHand.MAIN_HAND||p.isSpectator()||!WorldgenLab.anchors186(level.getChunkSource().getGenerator()))return InteractionResult.PASS;
var pos=hit.getBlockPos();var block=level.getBlockState(pos);
if(!block.is(ExpansionAnchorBlock.BLOCK))return InteractionResult.PASS;
int sector=block.getValue(ExpansionAnchorBlock.SECTOR);
var result=offer(level,pos,p.getItemInHand(hand));var demand=DEMANDS.get(sector);
String key=switch(result){case CORE_OFF->"core_off";case ACTIVATED->"activated";case ALREADY_ACTIVE->"already";case INVALID->"invalid";default->"required";};
p.sendSystemMessage(Component.translatable("sanctuary.palace."+key,
Component.translatable("sanctuary.palace.direction."+sector),demand.count(),new ItemStack(demand.item()).getHoverName()),true);
return InteractionResult.SUCCESS;
});
}
static PalaceAnchors.Result offer(ServerLevel level,BlockPos pos,ItemStack held){
var g=level.getChunkSource().getGenerator();if(!WorldgenLab.anchors186(g))return PalaceAnchors.Result.INVALID;
var site=Anchors186.plan(Anchors186.source(g),level.getSeed()).sites().stream().filter(s->s.anchor().equals(pos)).findFirst().orElse(null);
if(site==null)return PalaceAnchors.Result.INVALID;
var anchor=level.getBlockState(pos);
if(!anchor.is(ExpansionAnchorBlock.BLOCK)||anchor.getValue(ExpansionAnchorBlock.SECTOR)!=site.sector())return PalaceAnchors.Result.INVALID;
if(anchor.getValue(ExpansionAnchorBlock.LIT))return PalaceAnchors.Result.ALREADY_ACTIVE;
var core=level.getBlockState(CORE);
if(!core.is(OriginBlock.BLOCK))return PalaceAnchors.Result.INVALID;
if(!core.getValue(OriginBlock.LIT))return PalaceAnchors.Result.CORE_OFF;
var demand=DEMANDS.get(site.sector());
if(!held.is(demand.item())||held.getCount()<demand.count())return PalaceAnchors.Result.REQUIRED;
level.setBlock(CORE,OriginBlock.relay(core,site.sector(),true),Block.UPDATE_ALL);
level.setBlock(pos,anchor.setValue(ExpansionAnchorBlock.LIT,true),Block.UPDATE_ALL);
held.shrink(demand.count());return PalaceAnchors.Result.ACTIVATED;
}
static Map<String,Object> check(ServerLevel level,List<Anchors186.Site> sites){
var before=level.getBlockState(CORE);var anchors=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();
for(var s:sites)anchors.put(s.anchor(),level.getBlockState(s.anchor()));
try {
level.setBlock(CORE,before.setValue(OriginBlock.RELAYS,0).setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
int mask=0;
for(int i:new int[]{7,2,5,0,6,1,4,3}){
var site=sites.get(i);var demand=DEMANDS.get(i);
level.setBlock(site.anchor(),anchors.get(site.anchor()).setValue(ExpansionAnchorBlock.LIT,false),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);var shortStack=new ItemStack(demand.item(),demand.count()-1);
if(offer(level,site.anchor(),wrong)!=PalaceAnchors.Result.REQUIRED||wrong.getCount()!=64
||offer(level,site.anchor(),shortStack)!=PalaceAnchors.Result.REQUIRED||shortStack.getCount()!=demand.count()-1)throw new IllegalStateException("Invalid offer consumed");
var held=new ItemStack(demand.item(),demand.count()+1);
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ACTIVATED||held.getCount()!=1)throw new IllegalStateException("Valid offer failed");
mask|=1<<i;
if(level.getBlockState(CORE).getValue(OriginBlock.RELAYS)!=mask||!level.getBlockState(site.anchor()).getValue(ExpansionAnchorBlock.LIT))throw new IllegalStateException("Wrong relay bit");
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ALREADY_ACTIVE||held.getCount()!=1)throw new IllegalStateException("Repeated offer consumed");
}
return Map.of("distinctMaterials",8,"relayMask",mask,"invalidAndRepeatedOffersPreserved",true);
} finally {
level.setBlock(CORE,before,Block.UPDATE_ALL);anchors.forEach((p,b)->level.setBlock(p,b,Block.UPDATE_ALL));
}
}
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.*;
import java.util.*;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.*;
import net.minecraft.world.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Block;
/** Local offers for this new preset only. Native block states own progress; no expansion is activated. */
final class AnchorOffers187 {
static final BlockPos CORE=new BlockPos(0,640,0);
static final List<PalaceAnchors.Demand> DEMANDS=List.of(
new PalaceAnchors.Demand(Items.STONE,32),new PalaceAnchors.Demand(Items.OAK_LOG,16),
new PalaceAnchors.Demand(Items.WHEAT,16),new PalaceAnchors.Demand(Items.COPPER_INGOT,16),
new PalaceAnchors.Demand(Items.COAL,16),new PalaceAnchors.Demand(Items.AMETHYST_SHARD,8),
new PalaceAnchors.Demand(Items.IRON_INGOT,8),new PalaceAnchors.Demand(Items.GLASS,16));
private AnchorOffers187() {}
static void register(){
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
if(!(player instanceof ServerPlayer p)||!(world instanceof ServerLevel level)
||hand!=InteractionHand.MAIN_HAND||p.isSpectator()||!WorldgenLab.natural187(level.getChunkSource().getGenerator()))return InteractionResult.PASS;
var pos=hit.getBlockPos();var block=level.getBlockState(pos);
if(!block.is(ExpansionAnchorBlock.BLOCK))return InteractionResult.PASS;
int sector=block.getValue(ExpansionAnchorBlock.SECTOR);
var result=offer(level,pos,p.getItemInHand(hand));var demand=DEMANDS.get(sector);
String key=switch(result){case CORE_OFF->"core_off";case ACTIVATED->"activated";case ALREADY_ACTIVE->"already";case INVALID->"invalid";default->"required";};
p.sendSystemMessage(Component.translatable("sanctuary.palace."+key,
Component.translatable("sanctuary.palace.direction."+sector),demand.count(),new ItemStack(demand.item()).getHoverName()),true);
return InteractionResult.SUCCESS;
});
}
static PalaceAnchors.Result offer(ServerLevel level,BlockPos pos,ItemStack held){
var g=level.getChunkSource().getGenerator();if(!WorldgenLab.natural187(g))return PalaceAnchors.Result.INVALID;
var site=Anchors187.plan(Anchors187.source(g),level.getSeed()).sites().stream().filter(s->s.anchor().equals(pos)).findFirst().orElse(null);
if(site==null)return PalaceAnchors.Result.INVALID;
var anchor=level.getBlockState(pos);
if(!anchor.is(ExpansionAnchorBlock.BLOCK)||anchor.getValue(ExpansionAnchorBlock.SECTOR)!=site.sector())return PalaceAnchors.Result.INVALID;
if(anchor.getValue(ExpansionAnchorBlock.LIT))return PalaceAnchors.Result.ALREADY_ACTIVE;
var core=level.getBlockState(CORE);
if(!core.is(OriginBlock.BLOCK))return PalaceAnchors.Result.INVALID;
if(!core.getValue(OriginBlock.LIT))return PalaceAnchors.Result.CORE_OFF;
var demand=DEMANDS.get(site.sector());
if(!held.is(demand.item())||held.getCount()<demand.count())return PalaceAnchors.Result.REQUIRED;
level.setBlock(CORE,OriginBlock.relay(core,site.sector(),true),Block.UPDATE_ALL);
level.setBlock(pos,anchor.setValue(ExpansionAnchorBlock.LIT,true),Block.UPDATE_ALL);
held.shrink(demand.count());return PalaceAnchors.Result.ACTIVATED;
}
static Map<String,Object> check(ServerLevel level,List<Anchors187.Site> sites){
var before=level.getBlockState(CORE);var anchors=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();
for(var s:sites)anchors.put(s.anchor(),level.getBlockState(s.anchor()));
try {
level.setBlock(CORE,before.setValue(OriginBlock.RELAYS,0).setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
int mask=0;
for(int i:new int[]{7,2,5,0,6,1,4,3}){
var site=sites.get(i);var demand=DEMANDS.get(i);
level.setBlock(site.anchor(),anchors.get(site.anchor()).setValue(ExpansionAnchorBlock.LIT,false),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);var shortStack=new ItemStack(demand.item(),demand.count()-1);
if(offer(level,site.anchor(),wrong)!=PalaceAnchors.Result.REQUIRED||wrong.getCount()!=64
||offer(level,site.anchor(),shortStack)!=PalaceAnchors.Result.REQUIRED||shortStack.getCount()!=demand.count()-1)throw new IllegalStateException("Invalid offer consumed");
var held=new ItemStack(demand.item(),demand.count()+1);
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ACTIVATED||held.getCount()!=1)throw new IllegalStateException("Valid offer failed");
mask|=1<<i;
if(level.getBlockState(CORE).getValue(OriginBlock.RELAYS)!=mask||!level.getBlockState(site.anchor()).getValue(ExpansionAnchorBlock.LIT))throw new IllegalStateException("Wrong relay bit");
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ALREADY_ACTIVE||held.getCount()!=1)throw new IllegalStateException("Repeated offer consumed");
}
return Map.of("distinctMaterials",8,"relayMask",mask,"invalidAndRepeatedOffersPreserved",true);
} finally {
level.setBlock(CORE,before,Block.UPDATE_ALL);anchors.forEach((p,b)->level.setBlock(p,b,Block.UPDATE_ALL));
}
}
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.*;
import java.util.*;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.*;
import net.minecraft.world.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Block;
/** Local offers for this new preset only. Native block states own progress; no expansion is activated. */
final class AnchorOffers188 {
static final BlockPos CORE=new BlockPos(0,640,0);
static final List<PalaceAnchors.Demand> DEMANDS=List.of(
new PalaceAnchors.Demand(Items.STONE,32),new PalaceAnchors.Demand(Items.OAK_LOG,16),
new PalaceAnchors.Demand(Items.WHEAT,16),new PalaceAnchors.Demand(Items.COPPER_INGOT,16),
new PalaceAnchors.Demand(Items.COAL,16),new PalaceAnchors.Demand(Items.AMETHYST_SHARD,8),
new PalaceAnchors.Demand(Items.IRON_INGOT,8),new PalaceAnchors.Demand(Items.GLASS,16));
private AnchorOffers188() {}
static void register(){
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
if(!(player instanceof ServerPlayer p)||!(world instanceof ServerLevel level)
||hand!=InteractionHand.MAIN_HAND||p.isSpectator()||!WorldgenLab.details188(level.getChunkSource().getGenerator()))return InteractionResult.PASS;
var pos=hit.getBlockPos();var block=level.getBlockState(pos);
if(!block.is(ExpansionAnchorBlock.BLOCK))return InteractionResult.PASS;
int sector=block.getValue(ExpansionAnchorBlock.SECTOR);
var result=offer(level,pos,p.getItemInHand(hand));var demand=DEMANDS.get(sector);
String key=switch(result){case CORE_OFF->"core_off";case ACTIVATED->"activated";case ALREADY_ACTIVE->"already";case INVALID->"invalid";default->"required";};
p.sendSystemMessage(Component.translatable("sanctuary.palace."+key,
Component.translatable("sanctuary.palace.direction."+sector),demand.count(),new ItemStack(demand.item()).getHoverName()),true);
return InteractionResult.SUCCESS;
});
}
static PalaceAnchors.Result offer(ServerLevel level,BlockPos pos,ItemStack held){
var g=level.getChunkSource().getGenerator();if(!WorldgenLab.details188(g))return PalaceAnchors.Result.INVALID;
var site=Anchors188.plan(Anchors188.source(g),level.getSeed()).sites().stream().filter(s->s.anchor().equals(pos)).findFirst().orElse(null);
if(site==null)return PalaceAnchors.Result.INVALID;
var anchor=level.getBlockState(pos);
if(!anchor.is(ExpansionAnchorBlock.BLOCK)||anchor.getValue(ExpansionAnchorBlock.SECTOR)!=site.sector())return PalaceAnchors.Result.INVALID;
if(anchor.getValue(ExpansionAnchorBlock.LIT))return PalaceAnchors.Result.ALREADY_ACTIVE;
var core=level.getBlockState(CORE);
if(!core.is(OriginBlock.BLOCK))return PalaceAnchors.Result.INVALID;
if(!core.getValue(OriginBlock.LIT))return PalaceAnchors.Result.CORE_OFF;
var demand=DEMANDS.get(site.sector());
if(!held.is(demand.item())||held.getCount()<demand.count())return PalaceAnchors.Result.REQUIRED;
level.setBlock(CORE,OriginBlock.relay(core,site.sector(),true),Block.UPDATE_ALL);
level.setBlock(pos,anchor.setValue(ExpansionAnchorBlock.LIT,true),Block.UPDATE_ALL);
held.shrink(demand.count());return PalaceAnchors.Result.ACTIVATED;
}
static Map<String,Object> check(ServerLevel level,List<Anchors188.Site> sites){
var before=level.getBlockState(CORE);var anchors=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();
for(var s:sites)anchors.put(s.anchor(),level.getBlockState(s.anchor()));
try {
level.setBlock(CORE,before.setValue(OriginBlock.RELAYS,0).setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
int mask=0;
for(int i:new int[]{7,2,5,0,6,1,4,3}){
var site=sites.get(i);var demand=DEMANDS.get(i);
level.setBlock(site.anchor(),anchors.get(site.anchor()).setValue(ExpansionAnchorBlock.LIT,false),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);var shortStack=new ItemStack(demand.item(),demand.count()-1);
if(offer(level,site.anchor(),wrong)!=PalaceAnchors.Result.REQUIRED||wrong.getCount()!=64
||offer(level,site.anchor(),shortStack)!=PalaceAnchors.Result.REQUIRED||shortStack.getCount()!=demand.count()-1)throw new IllegalStateException("Invalid offer consumed");
var held=new ItemStack(demand.item(),demand.count()+1);
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ACTIVATED||held.getCount()!=1)throw new IllegalStateException("Valid offer failed");
mask|=1<<i;
if(level.getBlockState(CORE).getValue(OriginBlock.RELAYS)!=mask||!level.getBlockState(site.anchor()).getValue(ExpansionAnchorBlock.LIT))throw new IllegalStateException("Wrong relay bit");
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ALREADY_ACTIVE||held.getCount()!=1)throw new IllegalStateException("Repeated offer consumed");
}
return Map.of("distinctMaterials",8,"relayMask",mask,"invalidAndRepeatedOffersPreserved",true);
} finally {
level.setBlock(CORE,before,Block.UPDATE_ALL);anchors.forEach((p,b)->level.setBlock(p,b,Block.UPDATE_ALL));
}
}
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.*;
import java.util.*;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.*;
import net.minecraft.world.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Block;
/** Local offers for this new preset only. Native block states own progress; no expansion is activated. */
final class AnchorOffers189 {
static final BlockPos CORE=new BlockPos(0,640,0);
static final List<PalaceAnchors.Demand> DEMANDS=List.of(
new PalaceAnchors.Demand(Items.STONE,32),new PalaceAnchors.Demand(Items.OAK_LOG,16),
new PalaceAnchors.Demand(Items.WHEAT,16),new PalaceAnchors.Demand(Items.COPPER_INGOT,16),
new PalaceAnchors.Demand(Items.COAL,16),new PalaceAnchors.Demand(Items.AMETHYST_SHARD,8),
new PalaceAnchors.Demand(Items.IRON_INGOT,8),new PalaceAnchors.Demand(Items.GLASS,16));
private AnchorOffers189() {}
static void register(){
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
if(!(player instanceof ServerPlayer p)||!(world instanceof ServerLevel level)
||hand!=InteractionHand.MAIN_HAND||p.isSpectator()||!WorldgenLab.finishedTerrain(level.getChunkSource().getGenerator()))return InteractionResult.PASS;
var pos=hit.getBlockPos();var block=level.getBlockState(pos);
if(!block.is(ExpansionAnchorBlock.BLOCK)||SharedIsland196.revision(level.getChunkSource().getGenerator())>=199)return InteractionResult.PASS;
int sector=block.getValue(ExpansionAnchorBlock.SECTOR);
var result=offer(level,pos,p.getItemInHand(hand));var demand=DEMANDS.get(sector);
String key=switch(result){case CORE_OFF->"core_off";case ACTIVATED->"activated";case ALREADY_ACTIVE->"already";case INVALID->"invalid";default->"required";};
p.sendSystemMessage(Component.translatable("sanctuary.palace."+key,
Component.translatable("sanctuary.palace.direction."+sector),demand.count(),new ItemStack(demand.item()).getHoverName()),true);
return InteractionResult.SUCCESS;
});
}
static PalaceAnchors.Result offer(ServerLevel level,BlockPos pos,ItemStack held){
var g=level.getChunkSource().getGenerator();if(!WorldgenLab.finishedTerrain(g))return PalaceAnchors.Result.INVALID;
var site=Anchors189.plan(Anchors189.source(g),level.getSeed()).sites().stream().filter(s->s.anchor().equals(pos)).findFirst().orElse(null);
if(site==null)return PalaceAnchors.Result.INVALID;
var anchor=level.getBlockState(pos);
if(!anchor.is(ExpansionAnchorBlock.BLOCK)||anchor.getValue(ExpansionAnchorBlock.SECTOR)!=site.sector())return PalaceAnchors.Result.INVALID;
if(anchor.getValue(ExpansionAnchorBlock.LIT))return PalaceAnchors.Result.ALREADY_ACTIVE;
var core=level.getBlockState(CORE);
if(!core.is(OriginBlock.BLOCK))return PalaceAnchors.Result.INVALID;
if(!core.getValue(OriginBlock.LIT))return PalaceAnchors.Result.CORE_OFF;
var demand=DEMANDS.get(site.sector());
if(!held.is(demand.item())||held.getCount()<demand.count())return PalaceAnchors.Result.REQUIRED;
level.setBlock(CORE,OriginBlock.relay(core,site.sector(),true),Block.UPDATE_ALL);
level.setBlock(pos,anchor.setValue(ExpansionAnchorBlock.LIT,true),Block.UPDATE_ALL);
held.shrink(demand.count());return PalaceAnchors.Result.ACTIVATED;
}
static Map<String,Object> check(ServerLevel level,List<Anchors189.Site> sites){
var before=level.getBlockState(CORE);var anchors=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();
for(var s:sites)anchors.put(s.anchor(),level.getBlockState(s.anchor()));
try {
level.setBlock(CORE,before.setValue(OriginBlock.RELAYS,0).setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
int mask=0;
for(int i:new int[]{7,2,5,0,6,1,4,3}){
var site=sites.get(i);var demand=DEMANDS.get(i);
level.setBlock(site.anchor(),anchors.get(site.anchor()).setValue(ExpansionAnchorBlock.LIT,false),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);var shortStack=new ItemStack(demand.item(),demand.count()-1);
if(offer(level,site.anchor(),wrong)!=PalaceAnchors.Result.REQUIRED||wrong.getCount()!=64
||offer(level,site.anchor(),shortStack)!=PalaceAnchors.Result.REQUIRED||shortStack.getCount()!=demand.count()-1)throw new IllegalStateException("Invalid offer consumed");
var held=new ItemStack(demand.item(),demand.count()+1);
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ACTIVATED||held.getCount()!=1)throw new IllegalStateException("Valid offer failed");
mask|=1<<i;
if(level.getBlockState(CORE).getValue(OriginBlock.RELAYS)!=mask||!level.getBlockState(site.anchor()).getValue(ExpansionAnchorBlock.LIT))throw new IllegalStateException("Wrong relay bit");
if(offer(level,site.anchor(),held)!=PalaceAnchors.Result.ALREADY_ACTIVE||held.getCount()!=1)throw new IllegalStateException("Repeated offer consumed");
}
return Map.of("distinctMaterials",8,"relayMask",mask,"invalidAndRepeatedOffersPreserved",true);
} finally {
level.setBlock(CORE,before,Block.UPDATE_ALL);anchors.forEach((p,b)->level.setBlock(p,b,Block.UPDATE_ALL));
}
}
}
@@ -0,0 +1,202 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.ExpansionAnchorBlock;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Eight seed-owned refuges. Planning reads density only; placement never loads a neighbour chunk. */
public final class Anchors186 {
enum Kind { SURFACE, CAVE, SKY }
record Site(int sector,BlockPos floor,Kind kind,int dx,int dz,int rx,int rz,boolean bright) {
BlockPos anchor(){return floor.above(2);}
BlockPos entrance(){return floor.offset(dx*11,1,dz*11);}
}
record Plan(List<Site> sites,Map<ChunkPos,Map<BlockPos,BlockState>> chunks,long millis) {}
private record Candidate(BlockPos floor,Kind kind,int dx,int dz,double score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Anchors186() {}
static AdventureBiomes180 source(ChunkGenerator g){return (AdventureBiomes180)((NoiseBasedChunkGenerator)g).getBiomeSource();}
static Plan plan(AdventureBiomes180 s,long seed){synchronized(PLANS){return PLANS.computeIfAbsent(s,key->create(s,seed));}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
private static boolean reserved(AdventureBiomes180 s,int x,int z){
if(Math.hypot(x,z)<88||Math.hypot(x+152,z-152)<52||Dungeon180.near(s,x,z,28))return true;
for(var b:Basins180.sites(s))if(Math.hypot(x-b.x(),z-b.z())<78)return true;
for(var p:NaturalWater184.plan(s).pools())if(Math.abs(x-p.seed().getX())<68&&Math.abs(z-p.seed().getZ())<68)return true;
return false;
}
private static Candidate candidate(AdventureBiomes180 s,int x,int y,int z,Kind kind,long seed){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+3,z))return null;
// Require existing material beneath the entire centre, including the small pedestal.
for(int dx=-3;dx<=3;dx+=3)for(int dz=-3;dz<=3;dz+=3)
if(!rock(s,x+dx,y-3,z+dz)||!rock(s,x+dx,y-5,z+dz))return null;
int best=-1,ex=0,ez=0;
for(var d:Direction.Plane.HORIZONTAL){
int score=0;boolean end=false;
for(int step=1;step<=11;step++){
int px=x+d.getStepX()*step,pz=z+d.getStepZ()*step;
if(!rock(s,px,y-3,pz))break;
if(!rock(s,px,y+1,pz)&&!rock(s,px,y+2,pz))score++;
if(step==11)end=!rock(s,px,y+1,pz)&&!rock(s,px,y+2,pz);
}
if(end)for(int step=2;step<=11;step++)for(int side=-1;side<=1;side++){
int bend=step>3&&step<9?1:0;
int px=x+d.getStepX()*step-d.getStepZ()*(bend+side),pz=z+d.getStepZ()*step+d.getStepX()*(bend+side);
if(!rock(s,px,y-3,pz))end=false;
}
if(end&&score>best){best=score;ex=d.getStepX();ez=d.getStepZ();}
}
if(best<7)return null;
int air=0,roof=0;
for(int dx=-6;dx<=6;dx+=3)for(int dz=-6;dz<=6;dz+=3){
if(!rock(s,x+dx,y+3,z+dz))air++;
if(rock(s,x+dx,y+12,z+dz)||rock(s,x+dx,y+24,z+dz))roof++;
}
if(kind==Kind.CAVE&&roof<8)return null;
double jitter=(mix(seed^((long)x*73428767)^((long)y*912931)^z)>>>11)*0x1.0p-53;
return new Candidate(new BlockPos(x,y,z),kind,ex,ez,air*.1+best*.15+jitter*9);
}
private static Plan create(AdventureBiomes180 s,long seed){
long started=System.nanoTime();var candidates=new ArrayList<Candidate>();
// Sparse fixed grid, finite vertical scan. No terrain regions, raycasts or runtime search.
for(int x=-300;x<=300;x+=12)for(int z=-300;z<=300;z+=12){
if(Math.hypot(x,z)>310||reserved(s,x,z))continue;
int top=s.field().surface(x,z);if(top<140)continue;
var surface=candidate(s,x,top,z,Kind.SURFACE,seed);if(surface!=null)candidates.add(surface);
for(int y=Math.min(top-18,236);y>=92;y--){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+4,z))continue;
var cave=candidate(s,x,y,z,Kind.CAVE,seed);if(cave!=null)candidates.add(cave);
}
}
for(var reef:s.field().base().reefs()){
if(reef.length()<30)continue;
for(int dx=-12;dx<=12;dx+=6)for(int dz=-12;dz<=12;dz+=6){
int x=reef.x()+dx,z=reef.z()+dz;
for(int y=reef.y()+reef.rise()+2;y>=reef.y()-5;y--){
if(!rock(s,x,y,z))continue;
var sky=candidate(s,x,y,z,Kind.SKY,seed);if(sky!=null)candidates.add(sky);break;
}
}
}
var chosen=new ArrayList<Site>();int skyCount=0;
// Two eligible aerial sectors are selected by seed; other sectors alternate cave and surface preference.
var aerial=new ArrayList<Integer>();
for(int sector=0;sector<8;sector++){final int k=sector;if(candidates.stream().anyMatch(c->c.kind==Kind.SKY&&Rosette183.sector(c.floor.getX(),c.floor.getZ())==k))aerial.add(k);}
aerial.sort(Comparator.comparingLong(k->mix(seed^k*0x186L)));
var skySectors=new HashSet<>(aerial.subList(0,Math.min(2,aerial.size())));
for(int sector=0;sector<8;sector++){
final int k=sector;Kind preferred=skySectors.contains(k)?Kind.SKY:((sector+(int)(seed&1))%2==0?Kind.CAVE:Kind.SURFACE);
var ranked=candidates.stream().filter(c->Rosette183.sector(c.floor.getX(),c.floor.getZ())==k)
.sorted(Comparator.<Candidate>comparingDouble(c->c.score+(c.kind==preferred?100:0)).reversed()).toList();
Candidate selected=null;
for(var c:ranked){
if(c.kind==Kind.SKY&&!skySectors.contains(k))continue;
if(chosen.stream().anyMatch(v->v.floor.distSqr(c.floor)<44*44))continue;
selected=c;break;
}
if(selected==null)throw new IllegalStateException("No naturally supported anchor in sector "+k+" for seed "+seed);
var c=selected;long grain=mix(seed^k*7919L);
chosen.add(new Site(k,c.floor,c.kind,c.dx,c.dz,6+(int)(grain&1),5+(int)((grain>>>1)&1),(grain&4)==0));
if(c.kind==Kind.SKY)skyCount++;
}
if(skyCount==0)throw new IllegalStateException("No supported aerial anchor for seed "+seed);
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
for(var site:chosen)for(var e:sculpt(s,site,seed).entrySet())
chunks.computeIfAbsent(new ChunkPos(e.getKey().getX()>>4,e.getKey().getZ()>>4),key->new LinkedHashMap<>()).put(e.getKey(),e.getValue());
chunks.replaceAll((k,v)->Map.copyOf(v));
return new Plan(List.copyOf(chosen),Map.copyOf(chunks),(System.nanoTime()-started)/1_000_000);
}
private static void put(Map<BlockPos,BlockState> m,BlockPos p,Block b){m.put(p,b.defaultBlockState());}
private static Map<BlockPos,BlockState> sculpt(AdventureBiomes180 s,Site site,long seed){
var m=new LinkedHashMap<BlockPos,BlockState>();var o=site.floor;int y=o.getY();
Block stone=site.kind==Kind.SKY?Blocks.CALCITE:site.kind==Kind.CAVE?Blocks.TUFF:Blocks.ANDESITE;
double phase=(mix(seed^site.sector) & 1023)/163.0;
for(int dx=-site.rx-2;dx<=site.rx+2;dx++)for(int dz=-site.rz-2;dz<=site.rz+2;dz++){
int x=o.getX()+dx,z=o.getZ()+dz;
double d=dx*dx/(double)(site.rx*site.rx)+dz*dz/(double)(site.rz*site.rz)+.12*Math.sin(dx*.7+phase)*Math.cos(dz*.6);
if(d>1.15)continue;
int floor=y+(Math.abs(dx)<=3&&Math.abs(dz)<=3?0:(int)Math.round(.8*Math.sin(dx*.5+phase)*Math.cos(dz*.5)));
if(!rock(s,x,floor-3,z))continue; // Keep the edge of the host landform instead of hanging a platform over void.
var at=new BlockPos(x,floor,z);
if(d<=1){
for(int h=-2;h<=0;h++)put(m,at.above(h),h==0&&Math.floorMod(x+z+site.sector,5)==0?Blocks.MOSS_BLOCK:stone);
int height=3+(int)(4*Math.sqrt(Math.max(0,1-d)));
for(int h=1;h<=height;h++)put(m,at.above(h),Blocks.AIR);
if(d>.62&&Math.floorMod(x*7+z*13,11)==0){
put(m,at,Blocks.MOSS_BLOCK);put(m,at.above(),Blocks.FLOWERING_AZALEA);
}
}else {
// Short reliefs follow surviving rock faces; no closed constructed shell.
for(int h=0;h<=5;h++)if(rock(s,x,floor+h,z))
put(m,at.above(h),h==2&&Math.floorMod(x+z,3)==0?Blocks.CHISELED_TUFF:h==0?Blocks.MOSS_BLOCK:stone);
}
}
// A slight bend opens to an actual, dry natural floor found during selection.
for(int step=2;step<=11;step++){
int bend=step>3&&step<9?1:0;
int x=o.getX()+site.dx*step-site.dz*bend,z=o.getZ()+site.dz*step+site.dx*bend;
for(int side=-1;side<=1;side++){
var p=new BlockPos(x-site.dz*side,y,z+site.dx*side);
for(int h=-2;h<=0;h++)put(m,p.above(h),h==0?Blocks.MOSS_BLOCK:stone);
for(int h=1;h<=4;h++)put(m,p.above(h),Blocks.AIR);
}
}
// Low eroded crests shelter surface sites; all are rooted in the existing mass.
for(int side:new int[]{-1,1}){
int dx=-site.dx*4-site.dz*side*3,dz=-site.dz*4+site.dx*side*3;
var base=o.offset(dx,0,dz);if(!rock(s,base.getX(),y-3,base.getZ()))continue;
for(int h=-2;h<=3+(site.sector+side&1);h++)put(m,base.above(h),h==2?Blocks.CHISELED_TUFF:stone);
put(m,base.above(2).offset(site.dx,0,site.dz),Rosette183.GLASS[site.sector]);
if(site.bright)put(m,base.above(2),Blocks.SEA_LANTERN);
else put(m,base.above(3),Blocks.AMETHYST_BLOCK);
}
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
put(m,o.offset(dx,0,dz),Blocks.CHISELED_TUFF);
for(int h=1;h<=4;h++)put(m,o.offset(dx,h,dz),Blocks.AIR);
}
put(m,o.above(),Rosette183.GLASS[site.sector]);
m.put(site.anchor(),ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,site.sector));
return m;
}
public static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess chunk){
if(!WorldgenLab.anchors186(g))return;
var entries=plan(source(g),level.getSeed()).chunks.get(chunk.getPos());
if(entries!=null)entries.forEach(chunk::setBlockState);
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos pos){
if(!WorldgenLab.anchors186(g)||pos.getY()<88||pos.getY()>512||Math.abs(pos.getX())>325||Math.abs(pos.getZ())>325)return false;
var entries=plan(source(g),level.getSeed()).chunks.get(new ChunkPos(pos.getX()>>4,pos.getZ()>>4));
return entries!=null&&entries.containsKey(pos);
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 source){
var p=plan(source,level.getSeed());var again=create(source,level.getSeed());
if(!p.sites.equals(again.sites)||!p.chunks.equals(again.chunks))throw new IllegalStateException("Unstable anchor plan");
int count=0;var details=new ArrayList<Map<String,Object>>();
for(var cp:p.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var e:p.chunks.entrySet())for(var b:e.getValue().entrySet()){
if(!e.getKey().contains(b.getKey()))throw new IllegalStateException("Cross-chunk anchor write");
if(!level.getBlockState(b.getKey()).equals(b.getValue()))throw new IllegalStateException("Anchor refuge mismatch "+b.getKey()+" expected "+b.getValue()+" got "+level.getBlockState(b.getKey()));
if(b.getValue().is(ExpansionAnchorBlock.BLOCK))count++;
}
for(var site:p.sites){
if(Rosette183.sector(site.floor.getX(),site.floor.getZ())!=site.sector)throw new IllegalStateException("Wrong colour sector");
// Walk the full three-wide bent approach and verify its actual native support.
for(int step=2;step<=11;step++){
int bend=step>3&&step<9?1:0;var at=site.floor.offset(site.dx*step-site.dz*bend,1,site.dz*step+site.dx*bend);
if(!level.getBlockState(at).isAir()||!level.getBlockState(at.above()).isAir()||level.getBlockState(at.below()).getCollisionShape(level,at.below()).isEmpty())throw new IllegalStateException("Anchor approach blocked "+at);
}
if(!rock(source,site.floor.getX(),site.floor.getY()-5,site.floor.getZ()))throw new IllegalStateException("Anchor above void");
details.add(Map.of("sector",site.sector,"kind",site.kind.name(),"anchor",site.anchor().toShortString(),"entrance",site.entrance().toShortString(),"bright",site.bright));
}
if(count!=8||p.sites.stream().map(Site::sector).distinct().count()!=8)throw new IllegalStateException("Expected exactly eight anchors");
var offers=AnchorOffers186.check(level,p.sites);
return Map.of("sites",details,"planningMs",p.millis,"chunks",p.chunks.size(),"blocks",p.chunks.values().stream().mapToInt(Map::size).sum(),"deterministic",true,"offers",offers);
}
}
@@ -0,0 +1,205 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.ExpansionAnchorBlock;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Eight seed-owned refuges. Planning reads density only; placement never loads a neighbour chunk. */
public final class Anchors187 {
enum Kind { SURFACE, CAVE, SKY }
record Site(int sector,BlockPos floor,Kind kind,int dx,int dz,int rx,int rz,boolean bright) {
BlockPos anchor(){return floor.above(2);}
BlockPos entrance(){return floor.offset(dx*11,1,dz*11);}
}
record Plan(List<Site> sites,Map<ChunkPos,Map<BlockPos,BlockState>> chunks,long millis) {}
private record Candidate(BlockPos floor,Kind kind,int dx,int dz,double score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Anchors187() {}
static AdventureBiomes180 source(ChunkGenerator g){return (AdventureBiomes180)((NoiseBasedChunkGenerator)g).getBiomeSource();}
static Plan plan(AdventureBiomes180 s,long seed){synchronized(PLANS){return PLANS.computeIfAbsent(s,key->create(s,seed));}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
private static boolean reserved(AdventureBiomes180 s,int x,int z){
if(Math.hypot(x,z)<88||Dungeon180.near(s,x,z,28))return true;
for(var b:Basins180.sites(s))if(Math.hypot(x-b.x(),z-b.z())<78)return true;
for(var p:NaturalWater184.plan(s).pools())if(Math.abs(x-p.seed().getX())<68&&Math.abs(z-p.seed().getZ())<68)return true;
for(var p:SurfaceWater187.plan(s).pools())if(Math.abs(x-p.seed().getX())<80&&Math.abs(z-p.seed().getZ())<80)return true;
return false;
}
private static Candidate candidate(AdventureBiomes180 s,int x,int y,int z,Kind kind,long seed){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+3,z))return null;
// Require existing material beneath the entire centre, including the small pedestal.
for(int dx=-3;dx<=3;dx+=3)for(int dz=-3;dz<=3;dz+=3)
if(!rock(s,x+dx,y-3,z+dz)||!rock(s,x+dx,y-5,z+dz))return null;
int[][] inward={{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1}};
int sector=Rosette183.sector(x,z),ex=inward[sector][0],ez=inward[sector][1],best=0;
// The moss approach is inward. The opposite gate looks out along the sector's exact compass axis.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
for(int side=-1;side<=1;side++){
int px=x+ex*step-ez*(bend+side),pz=z+ez*step+ex*(bend+side);
if(!rock(s,px,y-3,pz))return null;
}
if(step>0&&!rock(s,x+ex*step,y+1,z+ez*step)&&!rock(s,x+ex*step,y+2,z+ez*step))best++;
}
if(best<7||rock(s,x+ex*11,y+1,z+ez*11)||rock(s,x+ex*11,y+2,z+ez*11))return null;
for(int side:new int[]{-1,1})if(!rock(s,x-ex*4-ez*side*3,y-3,z-ez*4+ex*side*3))return null;
int air=0,roof=0;
for(int dx=-6;dx<=6;dx+=3)for(int dz=-6;dz<=6;dz+=3){
if(!rock(s,x+dx,y+3,z+dz))air++;
if(rock(s,x+dx,y+12,z+dz)||rock(s,x+dx,y+24,z+dz))roof++;
}
if(kind==Kind.CAVE&&roof<8)return null;
double jitter=(mix(seed^((long)x*73428767)^((long)y*912931)^z)>>>11)*0x1.0p-53;
return new Candidate(new BlockPos(x,y,z),kind,ex,ez,air*.1+best*.15+jitter*9);
}
private static Plan create(AdventureBiomes180 s,long seed){
long started=System.nanoTime();var candidates=new ArrayList<Candidate>();
// Sparse fixed grid, finite vertical scan. No terrain regions, raycasts or runtime search.
for(int x=-300;x<=300;x+=12)for(int z=-300;z<=300;z+=12){
if(Math.hypot(x,z)>310||reserved(s,x,z))continue;
int top=s.field().surface(x,z);if(top<140)continue;
var surface=candidate(s,x,top,z,Kind.SURFACE,seed);if(surface!=null)candidates.add(surface);
for(int y=Math.min(top-18,236);y>=92;y--){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+4,z))continue;
var cave=candidate(s,x,y,z,Kind.CAVE,seed);if(cave!=null)candidates.add(cave);
}
}
for(var reef:s.field().base().reefs()){
if(reef.length()<30)continue;
for(int dx=-12;dx<=12;dx+=6)for(int dz=-12;dz<=12;dz+=6){
int x=reef.x()+dx,z=reef.z()+dz;
for(int y=reef.y()+reef.rise()+2;y>=reef.y()-5;y--){
if(!rock(s,x,y,z))continue;
var sky=candidate(s,x,y,z,Kind.SKY,seed);if(sky!=null)candidates.add(sky);break;
}
}
}
var chosen=new ArrayList<Site>();int skyCount=0;
// Two eligible aerial sectors are selected by seed; other sectors alternate cave and surface preference.
var aerial=new ArrayList<Integer>();
for(int sector=0;sector<8;sector++){final int k=sector;if(candidates.stream().anyMatch(c->c.kind==Kind.SKY&&Rosette183.sector(c.floor.getX(),c.floor.getZ())==k))aerial.add(k);}
aerial.sort(Comparator.comparingLong(k->mix(seed^k*0x186L)));
var skySectors=new HashSet<>(aerial.subList(0,Math.min(2,aerial.size())));
for(int sector=0;sector<8;sector++){
final int k=sector;Kind preferred=skySectors.contains(k)?Kind.SKY:((sector+(int)(seed&1))%2==0?Kind.CAVE:Kind.SURFACE);
var ranked=candidates.stream().filter(c->Rosette183.sector(c.floor.getX(),c.floor.getZ())==k)
.sorted(Comparator.<Candidate>comparingDouble(c->c.score+(c.kind==preferred?100:0)).reversed()).toList();
Candidate selected=null;
for(var c:ranked){
if(c.kind==Kind.SKY&&!skySectors.contains(k))continue;
if(chosen.stream().anyMatch(v->v.floor.distSqr(c.floor)<44*44))continue;
selected=c;break;
}
if(selected==null)throw new IllegalStateException("No naturally supported anchor in sector "+k+" for seed "+seed);
var c=selected;long grain=mix(seed^k*7919L);
chosen.add(new Site(k,c.floor,c.kind,c.dx,c.dz,6+(int)(grain&1),5+(int)((grain>>>1)&1),(grain&4)==0));
if(c.kind==Kind.SKY)skyCount++;
}
if(skyCount==0)throw new IllegalStateException("No supported aerial anchor for seed "+seed);
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
for(var site:chosen)for(var e:sculpt(s,site,seed).entrySet())
chunks.computeIfAbsent(new ChunkPos(e.getKey().getX()>>4,e.getKey().getZ()>>4),key->new LinkedHashMap<>()).put(e.getKey(),e.getValue());
chunks.replaceAll((k,v)->Map.copyOf(v));
return new Plan(List.copyOf(chosen),Map.copyOf(chunks),(System.nanoTime()-started)/1_000_000);
}
private static void put(Map<BlockPos,BlockState> m,BlockPos p,Block b){m.put(p,b.defaultBlockState());}
private static Map<BlockPos,BlockState> sculpt(AdventureBiomes180 s,Site site,long seed){
var m=new LinkedHashMap<BlockPos,BlockState>();var o=site.floor;int y=o.getY();
Block stone=site.kind==Kind.SKY?Blocks.CALCITE:site.kind==Kind.CAVE?Blocks.TUFF:Blocks.ANDESITE;
double phase=(mix(seed^site.sector) & 1023)/163.0;
for(int dx=-site.rx-2;dx<=site.rx+2;dx++)for(int dz=-site.rz-2;dz<=site.rz+2;dz++){
int x=o.getX()+dx,z=o.getZ()+dz;
double d=dx*dx/(double)(site.rx*site.rx)+dz*dz/(double)(site.rz*site.rz)+.12*Math.sin(dx*.7+phase)*Math.cos(dz*.6);
if(d>1.15)continue;
int floor=y+(Math.abs(dx)<=3&&Math.abs(dz)<=3?0:(int)Math.round(.8*Math.sin(dx*.5+phase)*Math.cos(dz*.5)));
if(!rock(s,x,floor-3,z))continue; // Keep the edge of the host landform instead of hanging a platform over void.
var at=new BlockPos(x,floor,z);
if(d<=1){
for(int h=-2;h<=0;h++)put(m,at.above(h),h==0&&Math.floorMod(x+z+site.sector,5)==0?Blocks.MOSS_BLOCK:stone);
int height=3+(int)(4*Math.sqrt(Math.max(0,1-d)));
for(int h=1;h<=height;h++)put(m,at.above(h),Blocks.AIR);
if(d>.62&&Math.floorMod(x*7+z*13,11)==0){
put(m,at,Blocks.MOSS_BLOCK);put(m,at.above(),Blocks.FLOWERING_AZALEA);
}
}else {
// Short reliefs follow surviving rock faces; no closed constructed shell.
for(int h=0;h<=5;h++)if(rock(s,x,floor+h,z))
put(m,at.above(h),h==2&&Math.floorMod(x+z,3)==0?Blocks.CHISELED_TUFF:h==0?Blocks.MOSS_BLOCK:stone);
}
}
// A slight bend opens to an actual, dry natural floor found during selection.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
int x=o.getX()+site.dx*step-site.dz*bend,z=o.getZ()+site.dz*step+site.dx*bend;
for(int side=-1;side<=1;side++){
var p=new BlockPos(x-site.dz*side,y,z+site.dx*side);
for(int h=-2;h<=0;h++)put(m,p.above(h),h==0&&step>=2?Blocks.MOSS_BLOCK:stone);
for(int h=1;h<=4;h++)put(m,p.above(h),Blocks.AIR);
}
}
// Low eroded crests shelter surface sites; all are rooted in the existing mass.
for(int side:new int[]{-1,1}){
int dx=-site.dx*4-site.dz*side*3,dz=-site.dz*4+site.dx*side*3;
var base=o.offset(dx,0,dz);if(!rock(s,base.getX(),y-3,base.getZ()))continue;
for(int h=-2;h<=2;h++)put(m,base.above(h),h==1?Blocks.CHISELED_TUFF:stone);
if(site.bright)put(m,base,Blocks.SEA_LANTERN);
put(m,base.above(3),Blocks.AMETHYST_BLOCK);
put(m,base.above(4),Rosette183.GLASS[site.sector]);
}
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
put(m,o.offset(dx,0,dz),Blocks.CHISELED_TUFF);
for(int h=1;h<=4;h++)put(m,o.offset(dx,h,dz),Blocks.AIR);
}
put(m,o.above(),Rosette183.GLASS[site.sector]);
m.put(site.anchor(),ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,site.sector));
return m;
}
public static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess chunk){
if(!WorldgenLab.natural187(g))return;
var entries=plan(source(g),level.getSeed()).chunks.get(chunk.getPos());
if(entries!=null)entries.forEach(chunk::setBlockState);
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos pos){
if(!WorldgenLab.natural187(g)||pos.getY()<88||pos.getY()>512||Math.abs(pos.getX())>325||Math.abs(pos.getZ())>325)return false;
var entries=plan(source(g),level.getSeed()).chunks.get(new ChunkPos(pos.getX()>>4,pos.getZ()>>4));
return entries!=null&&entries.containsKey(pos);
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 source){
var p=plan(source,level.getSeed());var again=create(source,level.getSeed());
if(!p.sites.equals(again.sites)||!p.chunks.equals(again.chunks))throw new IllegalStateException("Unstable anchor plan");
int count=0;var details=new ArrayList<Map<String,Object>>();
for(var cp:p.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var e:p.chunks.entrySet())for(var b:e.getValue().entrySet()){
if(!e.getKey().contains(b.getKey()))throw new IllegalStateException("Cross-chunk anchor write");
if(!level.getBlockState(b.getKey()).equals(b.getValue()))throw new IllegalStateException("Anchor refuge mismatch "+b.getKey()+" expected "+b.getValue()+" got "+level.getBlockState(b.getKey()));
if(b.getValue().is(ExpansionAnchorBlock.BLOCK))count++;
}
for(var site:p.sites){
if(Rosette183.sector(site.floor.getX(),site.floor.getZ())!=site.sector)throw new IllegalStateException("Wrong colour sector");
// Walk the full three-wide bent approach and verify its actual native support.
for(int step=-6;step<=11;step++){
if(step>=-1&&step<=1)continue;
int bend=step>3&&step<9?1:0;var at=site.floor.offset(site.dx*step-site.dz*bend,1,site.dz*step+site.dx*bend);
if(!level.getBlockState(at).isAir()||!level.getBlockState(at.above()).isAir()||level.getBlockState(at.below()).getCollisionShape(level,at.below()).isEmpty())throw new IllegalStateException("Anchor approach blocked "+at);
}
if(!rock(source,site.floor.getX(),site.floor.getY()-5,site.floor.getZ()))throw new IllegalStateException("Anchor above void");
if(site.dx*site.floor.getX()+site.dz*site.floor.getZ()>=0)throw new IllegalStateException("Approach points outward");
for(int side:new int[]{-1,1}){
var pylon=site.floor.offset(-site.dx*4-site.dz*side*3,0,-site.dz*4+site.dx*side*3);
if(!level.getBlockState(pylon.above(3)).is(Blocks.AMETHYST_BLOCK)||!level.getBlockState(pylon.above(4)).is(Rosette183.GLASS[site.sector]))throw new IllegalStateException("Pylon glass must sit on amethyst");
}
details.add(Map.of("sector",site.sector,"kind",site.kind.name(),"anchor",site.anchor().toShortString(),"entrance",site.entrance().toShortString(),"bright",site.bright));
}
if(count!=8||p.sites.stream().map(Site::sector).distinct().count()!=8)throw new IllegalStateException("Expected exactly eight anchors");
var offers=AnchorOffers187.check(level,p.sites);
return Map.of("sites",details,"planningMs",p.millis,"chunks",p.chunks.size(),"blocks",p.chunks.values().stream().mapToInt(Map::size).sum(),"deterministic",true,"offers",offers);
}
}
@@ -0,0 +1,218 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.ExpansionAnchorBlock;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Eight seed-owned refuges. Planning reads density only; placement never loads a neighbour chunk. */
public final class Anchors188 {
enum Kind { SURFACE, CAVE, SKY }
record Site(int sector,BlockPos floor,Kind kind,int dx,int dz,int rx,int rz,boolean bright) {
BlockPos anchor(){return floor.above(2);}
BlockPos entrance(){return floor.offset(dx*11,1,dz*11);}
}
record Plan(List<Site> sites,Map<ChunkPos,Map<BlockPos,BlockState>> chunks,long millis) {}
private record Candidate(BlockPos floor,Kind kind,int dx,int dz,double score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Anchors188() {}
static AdventureBiomes180 source(ChunkGenerator g){return (AdventureBiomes180)((NoiseBasedChunkGenerator)g).getBiomeSource();}
static Plan plan(AdventureBiomes180 s,long seed){synchronized(PLANS){return PLANS.computeIfAbsent(s,key->create(s,seed));}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
private static boolean reserved(AdventureBiomes180 s,int x,int z){
if(Math.hypot(x,z)<88||Dungeon180.near(s,x,z,28))return true;
for(var b:Basins180.sites(s))if(Math.hypot(x-b.x(),z-b.z())<78)return true;
for(var p:NaturalWater184.plan(s).pools())if(Math.abs(x-p.seed().getX())<68&&Math.abs(z-p.seed().getZ())<68)return true;
for(var p:SurfaceWater187.plan(s).pools())if(Math.abs(x-p.seed().getX())<80&&Math.abs(z-p.seed().getZ())<80)return true;
return false;
}
private static Candidate candidate(AdventureBiomes180 s,int x,int y,int z,Kind kind,long seed){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+3,z))return null;
// Require existing material beneath the entire centre, including the small pedestal.
for(int dx=-3;dx<=3;dx+=3)for(int dz=-3;dz<=3;dz+=3)
if(!rock(s,x+dx,y-3,z+dz)||!rock(s,x+dx,y-5,z+dz))return null;
int[][] inward={{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1}};
int sector=Rosette183.sector(x,z),ex=inward[sector][0],ez=inward[sector][1],best=0;
// The moss approach is inward. The opposite gate looks out along the sector's exact compass axis.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
for(int side=-1;side<=1;side++){
int px=x+ex*step-ez*(bend+side),pz=z+ez*step+ex*(bend+side);
if(!rock(s,px,y-3,pz))return null;
}
if(step>0&&!rock(s,x+ex*step,y+1,z+ez*step)&&!rock(s,x+ex*step,y+2,z+ez*step))best++;
}
if(best<7||rock(s,x+ex*11,y+1,z+ez*11)||rock(s,x+ex*11,y+2,z+ez*11))return null;
for(int side:new int[]{-1,1})if(!rock(s,x-ex*4-ez*side*3,y-3,z-ez*4+ex*side*3))return null;
int air=0,roof=0;
for(int dx=-6;dx<=6;dx+=3)for(int dz=-6;dz<=6;dz+=3){
if(!rock(s,x+dx,y+3,z+dz))air++;
if(rock(s,x+dx,y+12,z+dz)||rock(s,x+dx,y+24,z+dz))roof++;
}
if(kind==Kind.CAVE&&roof<8)return null;
double jitter=(mix(seed^((long)x*73428767)^((long)y*912931)^z)>>>11)*0x1.0p-53;
return new Candidate(new BlockPos(x,y,z),kind,ex,ez,air*.1+best*.15+jitter*9);
}
private static Plan create(AdventureBiomes180 s,long seed){
long started=System.nanoTime();var candidates=new ArrayList<Candidate>();
// Sparse fixed grid, finite vertical scan. No terrain regions, raycasts or runtime search.
for(int x=-300;x<=300;x+=12)for(int z=-300;z<=300;z+=12){
if(Math.hypot(x,z)>310||reserved(s,x,z))continue;
int top=s.field().surface(x,z);if(top<140)continue;
var surface=candidate(s,x,top,z,Kind.SURFACE,seed);if(surface!=null)candidates.add(surface);
for(int y=Math.min(top-18,236);y>=92;y--){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+4,z))continue;
var cave=candidate(s,x,y,z,Kind.CAVE,seed);if(cave!=null)candidates.add(cave);
}
}
for(var reef:s.field().base().reefs()){
if(reef.length()<30)continue;
for(int dx=-12;dx<=12;dx+=6)for(int dz=-12;dz<=12;dz+=6){
int x=reef.x()+dx,z=reef.z()+dz;
for(int y=reef.y()+reef.rise()+2;y>=reef.y()-5;y--){
if(!rock(s,x,y,z))continue;
var sky=candidate(s,x,y,z,Kind.SKY,seed);if(sky!=null)candidates.add(sky);break;
}
}
}
var chosen=new ArrayList<Site>();int skyCount=0;
// Two eligible aerial sectors are selected by seed; other sectors alternate cave and surface preference.
var aerial=new ArrayList<Integer>();
for(int sector=0;sector<8;sector++){final int k=sector;if(candidates.stream().anyMatch(c->c.kind==Kind.SKY&&Rosette183.sector(c.floor.getX(),c.floor.getZ())==k))aerial.add(k);}
aerial.sort(Comparator.comparingLong(k->mix(seed^k*0x186L)));
var skySectors=new HashSet<>(aerial.subList(0,Math.min(2,aerial.size())));
for(int sector=0;sector<8;sector++){
final int k=sector;Kind preferred=skySectors.contains(k)?Kind.SKY:((sector+(int)(seed&1))%2==0?Kind.CAVE:Kind.SURFACE);
var ranked=candidates.stream().filter(c->Rosette183.sector(c.floor.getX(),c.floor.getZ())==k)
.sorted(Comparator.<Candidate>comparingDouble(c->c.score+(c.kind==preferred?100:0)).reversed()).toList();
Candidate selected=null;
for(var c:ranked){
if(c.kind==Kind.SKY&&!skySectors.contains(k))continue;
if(chosen.stream().anyMatch(v->v.floor.distSqr(c.floor)<44*44))continue;
selected=c;break;
}
if(selected==null)throw new IllegalStateException("No naturally supported anchor in sector "+k+" for seed "+seed);
var c=selected;long grain=mix(seed^k*7919L);
chosen.add(new Site(k,c.floor,c.kind,c.dx,c.dz,6+(int)(grain&1),5+(int)((grain>>>1)&1),(grain&4)==0));
if(c.kind==Kind.SKY)skyCount++;
}
if(skyCount==0)throw new IllegalStateException("No supported aerial anchor for seed "+seed);
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
for(var site:chosen)for(var e:sculpt(s,site,seed).entrySet())
chunks.computeIfAbsent(new ChunkPos(e.getKey().getX()>>4,e.getKey().getZ()>>4),key->new LinkedHashMap<>()).put(e.getKey(),e.getValue());
chunks.replaceAll((k,v)->Map.copyOf(v));
return new Plan(List.copyOf(chosen),Map.copyOf(chunks),(System.nanoTime()-started)/1_000_000);
}
private static void put(Map<BlockPos,BlockState> m,BlockPos p,Block b){m.put(p,b.defaultBlockState());}
private static Map<BlockPos,BlockState> sculpt(AdventureBiomes180 s,Site site,long seed){
var m=new LinkedHashMap<BlockPos,BlockState>();var o=site.floor;int y=o.getY();
Block stone=site.kind==Kind.SKY?Blocks.CALCITE:site.kind==Kind.CAVE?Blocks.TUFF:Blocks.ANDESITE;
Block accent=switch(site.sector){case 0,7->Blocks.CALCITE;case 1,2->Blocks.MOSS_BLOCK;case 3->Blocks.TERRACOTTA;case 4->Blocks.DYED_TERRACOTTA.red();case 5->Blocks.AMETHYST_BLOCK;default->Blocks.DYED_TERRACOTTA.yellow();};
double phase=(mix(seed^site.sector) & 1023)/163.0;
for(int dx=-site.rx-2;dx<=site.rx+2;dx++)for(int dz=-site.rz-2;dz<=site.rz+2;dz++){
int x=o.getX()+dx,z=o.getZ()+dz;
double d=dx*dx/(double)(site.rx*site.rx)+dz*dz/(double)(site.rz*site.rz)+.12*Math.sin(dx*.7+phase)*Math.cos(dz*.6);
if(d>1.15)continue;
int floor=y+(Math.abs(dx)<=3&&Math.abs(dz)<=3?0:(int)Math.round(.8*Math.sin(dx*.5+phase)*Math.cos(dz*.5)));
if(!rock(s,x,floor-3,z))continue; // Keep the edge of the host landform instead of hanging a platform over void.
var at=new BlockPos(x,floor,z);
if(d<=1){
for(int h=-2;h<=0;h++)put(m,at.above(h),h==0&&Math.floorMod(x+z+site.sector,5)==0?Blocks.MOSS_BLOCK:stone);
int height=3+(int)(4*Math.sqrt(Math.max(0,1-d)));
for(int h=1;h<=height;h++)put(m,at.above(h),Blocks.AIR);
if(d>.62&&Math.floorMod(x*7+z*13,11)==0){
put(m,at,Blocks.MOSS_BLOCK);put(m,at.above(),Blocks.FLOWERING_AZALEA);
}
}else {
// Short reliefs follow surviving rock faces; no closed constructed shell.
for(int h=0;h<=5;h++)if(rock(s,x,floor+h,z))
put(m,at.above(h),h==2&&Math.floorMod(x+z,3)==0?Blocks.CHISELED_TUFF:h==0?Blocks.MOSS_BLOCK:stone);
}
}
// A slight bend opens to an actual, dry natural floor found during selection.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
int x=o.getX()+site.dx*step-site.dz*bend,z=o.getZ()+site.dz*step+site.dx*bend;
for(int side=-1;side<=1;side++){
var p=new BlockPos(x-site.dz*side,y,z+site.dx*side);
for(int h=-2;h<=0;h++)put(m,p.above(h),h==0&&step>=2?Blocks.MOSS_BLOCK:stone);
for(int h=1;h<=4;h++)put(m,p.above(h),Blocks.AIR);
}
}
// Low eroded crests shelter surface sites; all are rooted in the existing mass.
for(int side:new int[]{-1,1}){
int dx=-site.dx*4-site.dz*side*3,dz=-site.dz*4+site.dx*side*3;
var base=o.offset(dx,0,dz);if(!rock(s,base.getX(),y-3,base.getZ()))continue;
for(int h=-2;h<=2;h++)put(m,base.above(h),h==1?Blocks.CHISELED_TUFF:stone);
if(site.bright)put(m,base,Blocks.SEA_LANTERN);
put(m,base.above(3),Blocks.AMETHYST_BLOCK);
put(m,base.above(4),Rosette183.GLASS[site.sector]);
}
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
put(m,o.offset(dx,0,dz),Blocks.CHISELED_TUFF);
for(int h=1;h<=4;h++)put(m,o.offset(dx,h,dz),Blocks.AIR);
}
put(m,o.above(),Rosette183.GLASS[site.sector]);
m.put(site.anchor(),ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,site.sector));
// Keep the accepted geometry; the host material and sector colour shape each refuge's palette.
m.replaceAll((p,b)->{
int h=p.getY()-y;
double patch=EcologyBiomes176.noise(seed^0x188AL,p.getX()/5.0,p.getZ()/5.0);
if(b.is(stone)){
if(site.kind==Kind.SURFACE&&h<=0)return (h<0?Blocks.DIRT:patch>.3?Blocks.COARSE_DIRT:patch<-.3?Blocks.MOSS_BLOCK:Blocks.GRASS_BLOCK).defaultBlockState();
if(site.kind==Kind.CAVE)return (patch>.30?Blocks.DEEPSLATE:patch<-.35?Blocks.CALCITE:Blocks.TUFF).defaultBlockState();
if(site.kind==Kind.SKY&&patch>.25)return Blocks.DIORITE.defaultBlockState();
}
if(b.is(Blocks.CHISELED_TUFF)&&patch>-.2)return accent.defaultBlockState();
return b;
});
return m;
}
public static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess chunk){
if(!WorldgenLab.details188(g))return;
var entries=plan(source(g),level.getSeed()).chunks.get(chunk.getPos());
if(entries!=null)entries.forEach(chunk::setBlockState);
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos pos){
if(!WorldgenLab.details188(g)||pos.getY()<88||pos.getY()>512||Math.abs(pos.getX())>325||Math.abs(pos.getZ())>325)return false;
var entries=plan(source(g),level.getSeed()).chunks.get(new ChunkPos(pos.getX()>>4,pos.getZ()>>4));
return entries!=null&&entries.containsKey(pos);
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 source){
var p=plan(source,level.getSeed());var again=create(source,level.getSeed());
if(!p.sites.equals(again.sites)||!p.chunks.equals(again.chunks))throw new IllegalStateException("Unstable anchor plan");
int count=0;var details=new ArrayList<Map<String,Object>>();
for(var cp:p.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var e:p.chunks.entrySet())for(var b:e.getValue().entrySet()){
if(!e.getKey().contains(b.getKey()))throw new IllegalStateException("Cross-chunk anchor write");
if(!level.getBlockState(b.getKey()).equals(b.getValue()))throw new IllegalStateException("Anchor refuge mismatch "+b.getKey()+" expected "+b.getValue()+" got "+level.getBlockState(b.getKey()));
if(b.getValue().is(ExpansionAnchorBlock.BLOCK))count++;
}
for(var site:p.sites){
if(Rosette183.sector(site.floor.getX(),site.floor.getZ())!=site.sector)throw new IllegalStateException("Wrong colour sector");
// Walk the full three-wide bent approach and verify its actual native support.
for(int step=-6;step<=11;step++){
if(step>=-1&&step<=1)continue;
int bend=step>3&&step<9?1:0;var at=site.floor.offset(site.dx*step-site.dz*bend,1,site.dz*step+site.dx*bend);
if(!level.getBlockState(at).isAir()||!level.getBlockState(at.above()).isAir()||level.getBlockState(at.below()).getCollisionShape(level,at.below()).isEmpty())throw new IllegalStateException("Anchor approach blocked "+at);
}
if(!rock(source,site.floor.getX(),site.floor.getY()-5,site.floor.getZ()))throw new IllegalStateException("Anchor above void");
if(site.dx*site.floor.getX()+site.dz*site.floor.getZ()>=0)throw new IllegalStateException("Approach points outward");
for(int side:new int[]{-1,1}){
var pylon=site.floor.offset(-site.dx*4-site.dz*side*3,0,-site.dz*4+site.dx*side*3);
if(!level.getBlockState(pylon.above(3)).is(Blocks.AMETHYST_BLOCK)||!level.getBlockState(pylon.above(4)).is(Rosette183.GLASS[site.sector]))throw new IllegalStateException("Pylon glass must sit on amethyst");
}
details.add(Map.of("sector",site.sector,"kind",site.kind.name(),"anchor",site.anchor().toShortString(),"entrance",site.entrance().toShortString(),"bright",site.bright));
}
if(count!=8||p.sites.stream().map(Site::sector).distinct().count()!=8)throw new IllegalStateException("Expected exactly eight anchors");
var offers=AnchorOffers188.check(level,p.sites);
return Map.of("sites",details,"planningMs",p.millis,"chunks",p.chunks.size(),"blocks",p.chunks.values().stream().mapToInt(Map::size).sum(),"deterministic",true,"offers",offers);
}
}
@@ -0,0 +1,335 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.ExpansionAnchorBlock;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Eight seed-owned refuges. Planning reads density only; placement never loads a neighbour chunk. */
public final class Anchors189 {
enum Kind { SURFACE, CAVE, SKY }
record Site(int sector,BlockPos floor,Kind kind,int dx,int dz,int rx,int rz,boolean bright) {
BlockPos anchor(){return floor.above(2);}
BlockPos entrance(){return floor.offset(dx*11,1,dz*11);}
}
record Plan(List<Site> sites,Map<ChunkPos,Map<BlockPos,BlockState>> chunks,long millis) {}
private record Candidate(BlockPos floor,Kind kind,int dx,int dz,double score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Anchors189() {}
static AdventureBiomes180 source(ChunkGenerator g){return (AdventureBiomes180)((NoiseBasedChunkGenerator)g).getBiomeSource();}
static Plan plan(AdventureBiomes180 s,long seed){synchronized(PLANS){return PLANS.computeIfAbsent(s,key->create(s,seed));}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
private static boolean reserved(AdventureBiomes180 s,int x,int z){
if(Math.hypot(x,z)<88||Dungeon180.near(s,x,z,28))return true;
for(var b:Basins180.sites(s))if(Math.hypot(x-b.x(),z-b.z())<78)return true;
for(var p:NaturalWater184.plan(s).pools())if(Math.abs(x-p.seed().getX())<68&&Math.abs(z-p.seed().getZ())<68)return true;
for(var p:SurfaceWater187.plan(s).pools())if(Math.abs(x-p.seed().getX())<80&&Math.abs(z-p.seed().getZ())<80)return true;
return false;
}
private static Candidate candidate(AdventureBiomes180 s,int x,int y,int z,Kind kind,long seed){return candidate(s,x,y,z,kind,seed,false);}
private static Candidate candidate(AdventureBiomes180 s,int x,int y,int z,Kind kind,long seed,boolean precise){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+3,z))return null;
if(s.field().base().capacity()!=fr.koka.sanctuary.worldgen.IslandCapacity.TEN&&(precise?reserved199(s,x,y,z):reservedAt(s,x,y,z)))return null;
// Require existing material beneath the entire centre, including the small pedestal.
for(int dx=-3;dx<=3;dx+=3)for(int dz=-3;dz<=3;dz+=3)
if(!rock(s,x+dx,y-3,z+dz)||!rock(s,x+dx,y-5,z+dz))return null;
int[][] inward={{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1},{1,0},{1,1}};
int sector=Rosette183.sector(x,z),ex=inward[sector][0],ez=inward[sector][1],best=0;
// The moss approach is inward. The opposite gate looks out along the sector's exact compass axis.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
for(int side=-1;side<=1;side++){
int px=x+ex*step-ez*(bend+side),pz=z+ez*step+ex*(bend+side);
if(!rock(s,px,y-3,pz))return null;
}
if(step>0&&!rock(s,x+ex*step,y+1,z+ez*step)&&!rock(s,x+ex*step,y+2,z+ez*step))best++;
}
if(s.field().coherent()&&kind!=Kind.CAVE){
int previous=y;
for(int step=0;step<=11;step++){
int bend=step>3&&step<9?1:0,px=x+ex*step-ez*bend,pz=z+ez*step+ex*bend;
int gy=step<=2?y:ground(s,px,pz,y);
if(!rock(s,px,gy,pz)||Math.abs(gy-previous)>1)return null;
for(int side=-1;side<=1;side++)if(Math.abs(ground(s,px-ez*side,pz+ex*side,y)-gy)>1)return null;
previous=gy;
}
}
if(best<7||rock(s,x+ex*11,y+1,z+ez*11)||rock(s,x+ex*11,y+2,z+ez*11))return null;
for(int side:new int[]{-1,1})if(!rock(s,x-ex*4-ez*side*3,y-3,z-ez*4+ex*side*3))return null;
int air=0,roof=0;
for(int dx=-6;dx<=6;dx+=3)for(int dz=-6;dz<=6;dz+=3){
if(!rock(s,x+dx,y+3,z+dz))air++;
if(rock(s,x+dx,y+12,z+dz)||rock(s,x+dx,y+24,z+dz))roof++;
}
if(kind==Kind.CAVE&&roof<8)return null;
double jitter=(mix(seed^((long)x*73428767)^((long)y*912931)^z)>>>11)*0x1.0p-53;
return new Candidate(new BlockPos(x,y,z),kind,ex,ez,air*.1+best*.15+jitter*9);
}
private static Plan create(AdventureBiomes180 s,long seed){
long started=System.nanoTime();var candidates=new ArrayList<Candidate>();
// Sparse fixed grid, finite vertical scan. No terrain regions, raycasts or runtime search.
double spread=s.field().base().spread();int step=(int)Math.round(12*spread),limit=25*step;
for(int x=-limit;x<=limit;x+=step)for(int z=-limit;z<=limit;z+=step){
if(Math.hypot(x,z)>310*spread||Math.hypot(x,z)<88
||s.field().base().capacity()==fr.koka.sanctuary.worldgen.IslandCapacity.TEN&&reserved(s,x,z))continue;
int top=s.field().surface(x,z);if(top<140)continue;
var surface=candidate(s,x,top,z,Kind.SURFACE,seed);if(surface!=null)candidates.add(surface);
for(int y=Math.min(top-18,236);y>=92;y--){
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+4,z))continue;
var cave=candidate(s,x,y,z,Kind.CAVE,seed);if(cave!=null)candidates.add(cave);
}
}
for(var reef:s.field().base().reefs()){
if(reef.length()<30)continue;
int extent=s.field().coherent()?20:12,spacing=s.field().coherent()?4:6;
for(int dx=-extent;dx<=extent;dx+=spacing)for(int dz=-extent;dz<=extent;dz+=spacing){
int x=reef.x()+dx,z=reef.z()+dz;
for(int y=reef.y()+reef.rise()+2;y>=reef.y()-5;y--){
if(!rock(s,x,y,z))continue;
var sky=candidate(s,x,y,z,Kind.SKY,seed);if(sky!=null)candidates.add(sky);break;
}
}
}
var chosen=new ArrayList<Site>();int skyCount=0;
// Two eligible aerial sectors are selected by seed; other sectors alternate cave and surface preference.
var aerial=new ArrayList<Integer>();
for(int sector=0;sector<8;sector++){final int k=sector;if(candidates.stream().anyMatch(c->c.kind==Kind.SKY&&Rosette183.sector(c.floor.getX(),c.floor.getZ())==k))aerial.add(k);}
aerial.sort(Comparator.comparingLong(k->mix(seed^k*0x186L)));
var skySectors=new HashSet<>(aerial.subList(0,Math.min(2,aerial.size())));
for(int sector=0;sector<8;sector++){
final int k=sector;Kind preferred=skySectors.contains(k)?Kind.SKY:((sector+(int)(seed&1))%2==0?Kind.CAVE:Kind.SURFACE);
var ranked=candidates.stream().filter(c->Rosette183.sector(c.floor.getX(),c.floor.getZ())==k)
.sorted(Comparator.<Candidate>comparingDouble(c->c.score+(c.kind==preferred?100:0)).reversed()).toList();
Candidate selected=null;
for(var c:ranked){
if(c.kind==Kind.SKY&&!skySectors.contains(k))continue;
if(chosen.stream().anyMatch(v->v.floor.distSqr(c.floor)<44*44))continue;
selected=c;break;
}
if(selected==null&&s.expansions!=null)selected=denseCandidate199(s,seed,k,chosen);
if(selected==null)throw new IllegalStateException("No naturally supported anchor in sector "+k+" for seed "+seed);
var c=selected;long grain=mix(seed^k*7919L);
chosen.add(new Site(k,c.floor,c.kind,c.dx,c.dz,6+(int)(grain&1),5+(int)((grain>>>1)&1),(grain&4)==0));
if(c.kind==Kind.SKY)skyCount++;
}
if(skyCount==0)throw new IllegalStateException("No supported aerial anchor for seed "+seed);
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
for(var site:chosen)for(var e:sculpt(s,site,seed).entrySet())
chunks.computeIfAbsent(new ChunkPos(e.getKey().getX()>>4,e.getKey().getZ()>>4),key->new LinkedHashMap<>()).put(e.getKey(),e.getValue());
chunks.replaceAll((k,v)->Map.copyOf(v));
return new Plan(List.copyOf(chosen),Map.copyOf(chunks),(System.nanoTime()-started)/1_000_000);
}
/** The coarse historical grid can miss narrow valid floors on Small. New worlds retry only the missing sector. */
private static Candidate denseCandidate199(AdventureBiomes180 s,long seed,int sector,List<Site> chosen){
int limit=(int)(310*s.field().base().spread());
for(int x=-limit;x<=limit;x+=3)for(int z=-limit;z<=limit;z+=3){
if(Rosette183.sector(x,z)!=sector||Math.hypot(x,z)<88||Math.hypot(x,z)>limit)continue;
int top=s.field().surface(x,z);if(top<140)continue;
for(int y=top;y>=64;y--){
if(y>top-18&&y!=top)continue;
if(!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+4,z))continue;
var at=new BlockPos(x,y,z);if(chosen.stream().anyMatch(v->v.floor.distSqr(at)<44*44))continue;
var candidate=candidate(s,x,y,z,y==top?Kind.SURFACE:Kind.CAVE,seed,true);
if(candidate!=null)return candidate;
}
}
return null;
}
private static final Map<AdventureBiomes180,List<net.minecraft.world.level.levelgen.structure.BoundingBox>> WATER199=Collections.synchronizedMap(new WeakHashMap<>());
private static boolean reserved199(AdventureBiomes180 s,int x,int y,int z){
var dungeon=Dungeon180.plan(s);
for(var room:dungeon.rooms())if(Math.abs(y-room.y())<20&&Math.abs(x-room.x())<room.rx()+18&&Math.abs(z-room.z())<room.rz()+18)return true;
for(var cell:dungeon.paths().values())if(Math.abs(y-cell.y())<16&&Math.abs(x-cell.x())<18&&Math.abs(z-cell.z())<18)return true;
if(dungeon.hut()!=null&&Math.abs(y-dungeon.hut().getY())<20&&Math.hypot(x-dungeon.hut().getX(),z-dungeon.hut().getZ())<28)return true;
for(var b:Basins180.sites(s)){
int u=(x-b.x())*b.dx()+(z-b.z())*b.dz(),v=-(x-b.x())*b.dz()+(z-b.z())*b.dx();
if(y>b.y()-28&&y<b.y()+14&&u>=-46&&u<=74&&Math.abs(v)<=39)return true;
}
var boxes=WATER199.computeIfAbsent(s,key->{
var out=new ArrayList<net.minecraft.world.level.levelgen.structure.BoundingBox>();
var bodies=new ArrayList<Set<BlockPos>>();
NaturalWater184.plan(s).pools().forEach(p->bodies.add(p.cells()));SurfaceWater187.plan(s).pools().forEach(p->bodies.add(p.cells()));
for(var cells:bodies)if(!cells.isEmpty()){
var b=net.minecraft.world.level.levelgen.structure.BoundingBox.encapsulatingPositions(cells).orElseThrow();
out.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(b.minX()-18,b.minY()-12,b.minZ()-18,b.maxX()+18,b.maxY()+12,b.maxZ()+18));
}
return List.copyOf(out);
});
return boxes.stream().anyMatch(b->b.isInside(x,y,z));
}
/** Small/Large may stack dry refuges above/below another site, but their volumes must stay separate. */
private static boolean reservedAt(AdventureBiomes180 s,int x,int y,int z){
var dungeon=Dungeon180.plan(s);
for(var room:dungeon.rooms())if(Math.abs(y-room.y())<20&&Math.abs(x-room.x())<room.rx()+18&&Math.abs(z-room.z())<room.rz()+18)return true;
for(var cell:dungeon.paths().values())if(Math.abs(y-cell.y())<16&&Math.abs(x-cell.x())<18&&Math.abs(z-cell.z())<18)return true;
if(dungeon.hut()!=null&&Math.abs(y-dungeon.hut().getY())<20&&Math.hypot(x-dungeon.hut().getX(),z-dungeon.hut().getZ())<28)return true;
for(var b:Basins180.sites(s))if(y>b.y()-28&&y<b.y()+14&&Math.hypot(x-b.x(),z-b.z())<90)return true;
for(var p:NaturalWater184.plan(s).pools())if(y>p.surface()-40&&y<p.surface()+14&&Math.abs(x-p.seed().getX())<70&&Math.abs(z-p.seed().getZ())<70)return true;
for(var p:SurfaceWater187.plan(s).pools())if(y>p.surface()-48&&y<p.surface()+14&&Math.abs(x-p.seed().getX())<90&&Math.abs(z-p.seed().getZ())<90)return true;
return false;
}
private static void put(Map<BlockPos,BlockState> m,BlockPos p,Block b){m.put(p,b.defaultBlockState());}
private static Map<BlockPos,BlockState> sculpt(AdventureBiomes180 s,Site site,long seed){
if(site.kind==Kind.SURFACE||s.field().coherent()&&site.kind==Kind.SKY)return surfaceSculpt(s,site,seed);
var m=new LinkedHashMap<BlockPos,BlockState>();var o=site.floor;int y=o.getY();
Block stone=site.kind==Kind.SKY?Blocks.CALCITE:site.kind==Kind.CAVE?Blocks.TUFF:Blocks.ANDESITE;
Block accent=switch(site.sector){case 0,7->Blocks.CALCITE;case 1,2->Blocks.MOSS_BLOCK;case 3->Blocks.TERRACOTTA;case 4->Blocks.DYED_TERRACOTTA.red();case 5->Blocks.AMETHYST_BLOCK;default->Blocks.DYED_TERRACOTTA.yellow();};
double phase=(mix(seed^site.sector) & 1023)/163.0;
for(int dx=-site.rx-2;dx<=site.rx+2;dx++)for(int dz=-site.rz-2;dz<=site.rz+2;dz++){
int x=o.getX()+dx,z=o.getZ()+dz;
double d=dx*dx/(double)(site.rx*site.rx)+dz*dz/(double)(site.rz*site.rz)+.12*Math.sin(dx*.7+phase)*Math.cos(dz*.6);
if(d>1.15)continue;
int floor=y+(Math.abs(dx)<=3&&Math.abs(dz)<=3?0:(int)Math.round(.8*Math.sin(dx*.5+phase)*Math.cos(dz*.5)));
if(!rock(s,x,floor-3,z))continue; // Keep the edge of the host landform instead of hanging a platform over void.
var at=new BlockPos(x,floor,z);
if(d<=1){
for(int h=-2;h<=0;h++)put(m,at.above(h),h==0&&EcologyBiomes176.noise(seed^0x189A0L,x/14.0,z/14.0)>.3?Blocks.MOSS_BLOCK:stone);
int height=3+(int)(4*Math.sqrt(Math.max(0,1-d)));
for(int h=1;h<=height;h++)put(m,at.above(h),Blocks.AIR);
if(d>.62&&Math.floorMod(x*7+z*13,11)==0){
put(m,at,Blocks.MOSS_BLOCK);put(m,at.above(),Blocks.FLOWERING_AZALEA);
}
}else {
// Short reliefs follow surviving rock faces; no closed constructed shell.
for(int h=0;h<=5;h++)if(rock(s,x,floor+h,z))
put(m,at.above(h),h==2&&Math.floorMod(x+z,3)==0?Blocks.CHISELED_TUFF:h==0?Blocks.MOSS_BLOCK:stone);
}
}
// A slight bend opens to an actual, dry natural floor found during selection.
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
int x=o.getX()+site.dx*step-site.dz*bend,z=o.getZ()+site.dz*step+site.dx*bend;
for(int side=-1;side<=1;side++){
var p=new BlockPos(x-site.dz*side,y,z+site.dx*side);
for(int h=-2;h<=0;h++)put(m,p.above(h),h==0&&step>=2?Blocks.MOSS_BLOCK:stone);
for(int h=1;h<=4;h++)put(m,p.above(h),Blocks.AIR);
}
}
// Low eroded crests shelter surface sites; all are rooted in the existing mass.
for(int side:new int[]{-1,1}){
int dx=-site.dx*4-site.dz*side*3,dz=-site.dz*4+site.dx*side*3;
var base=o.offset(dx,0,dz);if(!rock(s,base.getX(),y-3,base.getZ()))continue;
for(int h=-2;h<=2;h++)put(m,base.above(h),h==1?Blocks.CHISELED_TUFF:stone);
if(site.bright)put(m,base,Blocks.SEA_LANTERN);
put(m,base.above(3),Blocks.AMETHYST_BLOCK);
put(m,base.above(4),Rosette183.GLASS[site.sector]);
}
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
put(m,o.offset(dx,0,dz),Blocks.CHISELED_TUFF);
for(int h=1;h<=4;h++)put(m,o.offset(dx,h,dz),Blocks.AIR);
}
put(m,o.above(),Rosette183.GLASS[site.sector]);
m.put(site.anchor(),ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,site.sector));
// Keep the accepted geometry; the host material and sector colour shape each refuge's palette.
m.replaceAll((p,b)->{
int h=p.getY()-y;
double patch=EcologyBiomes176.noise(seed^0x188AL,p.getX()/5.0,p.getZ()/5.0);
if(b.is(stone)){
if(site.kind==Kind.SURFACE&&h<=0)return (h<0?Blocks.DIRT:patch<-.3?Blocks.MOSS_BLOCK:Blocks.GRASS_BLOCK).defaultBlockState();
if(site.kind==Kind.CAVE)return (patch>.30?Blocks.DEEPSLATE:patch<-.35?Blocks.CALCITE:Blocks.TUFF).defaultBlockState();
if(site.kind==Kind.SKY&&patch>.25)return Blocks.DIORITE.defaultBlockState();
}
if(b.is(Blocks.CHISELED_TUFF)&&patch>-.2)return accent.defaultBlockState();
return b;
});
return m;
}
static int ground(AdventureBiomes180 s,int x,int z,int near){
// Density only: neither leaves, trunks nor structures can become a soil support.
for(int y=near+12;y>=near-8;y--)if(rock(s,x,y,z)&&!rock(s,x,y+1,z))return y;
return near;
}
private static int pathY(AdventureBiomes180 s,Site site,int x,int z,int step){
return (site.kind==Kind.SURFACE||s.field().coherent()&&site.kind==Kind.SKY)&&Math.abs(step)>2?ground(s,x,z,site.floor.getY()):site.floor.getY();
}
private static BlockPos pylon(AdventureBiomes180 s,Site site,int side){
var p=site.floor.offset(-site.dx*4-site.dz*side*3,0,-site.dz*4+site.dx*side*3);
return (site.kind==Kind.SURFACE||s.field().coherent()&&site.kind==Kind.SKY)?new BlockPos(p.getX(),ground(s,p.getX(),p.getZ(),p.getY()),p.getZ()):p;
}
private static Map<BlockPos,BlockState> surfaceSculpt(AdventureBiomes180 s,Site site,long seed){
var m=new LinkedHashMap<BlockPos,BlockState>();var o=site.floor;
for(int dx=-site.rx-2;dx<=site.rx+2;dx++)for(int dz=-site.rz-2;dz<=site.rz+2;dz++){
double d=dx*dx/(double)(site.rx*site.rx)+dz*dz/(double)(site.rz*site.rz);
if(d>1)continue;int x=o.getX()+dx,z=o.getZ()+dz,y=ground(s,x,z,o.getY());
if(!rock(s,x,y-1,z))continue;
double patch=EcologyBiomes176.noise(seed^0x189A0L,x/14.0,z/14.0);
// One surface block, on the original rock. Broad moss only near the inward approach.
put(m,new BlockPos(x,y,z),!s.field().coherent()&&patch>.25?Blocks.MOSS_BLOCK:Blocks.GRASS_BLOCK);
}
for(int step=-6;step<=11;step++){
int bend=step>3&&step<9?1:0;
int x=o.getX()+site.dx*step-site.dz*bend,z=o.getZ()+site.dz*step+site.dx*bend;
for(int side=-1;side<=1;side++){
int px=x-site.dz*side,pz=z+site.dx*side;
int y=pathY(s,site,px,pz,step);var p=new BlockPos(px,y,pz);
for(int h=-2;h<=0;h++)if(h==0||!rock(s,px,y+h,pz))put(m,p.above(h),h==0?(step>=2?(s.field().coherent()?Blocks.COARSE_DIRT:Blocks.MOSS_BLOCK):Blocks.ANDESITE):Blocks.DIRT);
for(int h=1;h<=3;h++)put(m,p.above(h),Blocks.AIR);
}
}
for(int side:new int[]{-1,1}){
var p=pylon(s,site,side);
put(m,p,Blocks.ANDESITE);put(m,p.above(),Blocks.CHISELED_TUFF);put(m,p.above(2),Blocks.TUFF);
if(site.bright)put(m,p.above(2),Blocks.GLOWSTONE);
put(m,p.above(3),Blocks.AMETHYST_BLOCK);put(m,p.above(4),Rosette183.GLASS[site.sector]);
}
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++){
for(int h=-2;h<=0;h++)put(m,o.offset(dx,h,dz),h==0?Blocks.CHISELED_TUFF:Blocks.TUFF);
for(int h=1;h<=4;h++)put(m,o.offset(dx,h,dz),Blocks.AIR);
}
put(m,o.above(),Rosette183.GLASS[site.sector]);
m.put(site.anchor(),ExpansionAnchorBlock.BLOCK.defaultBlockState().setValue(ExpansionAnchorBlock.SECTOR,site.sector));
return m;
}
public static boolean treeBlocked(AdventureBiomes180 s,BlockPos p){
for(var a:plan(s,s.seed()).sites())if(Math.abs(p.getY()-a.floor.getY())<24&&Math.hypot(p.getX()-a.floor.getX(),p.getZ()-a.floor.getZ())<15)return true;
return false;
}
public static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess chunk){
if(!WorldgenLab.finishedTerrain(g))return;
var entries=plan(source(g),level.getSeed()).chunks.get(chunk.getPos());
if(entries!=null)entries.forEach(chunk::setBlockState);
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos pos){
if(!WorldgenLab.finishedTerrain(g)||pos.getY()<88||pos.getY()>512)return false;
var entries=plan(source(g),level.getSeed()).chunks.get(new ChunkPos(pos.getX()>>4,pos.getZ()>>4));
return entries!=null&&entries.containsKey(pos);
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 source){
var p=plan(source,level.getSeed());var again=create(source,level.getSeed());
if(!p.sites.equals(again.sites)||!p.chunks.equals(again.chunks))throw new IllegalStateException("Unstable anchor plan");
int count=0;var details=new ArrayList<Map<String,Object>>();
for(var cp:p.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var e:p.chunks.entrySet())for(var b:e.getValue().entrySet()){
if(!e.getKey().contains(b.getKey()))throw new IllegalStateException("Cross-chunk anchor write");
if(!level.getBlockState(b.getKey()).equals(b.getValue()))throw new IllegalStateException("Anchor refuge mismatch "+b.getKey()+" expected "+b.getValue()+" got "+level.getBlockState(b.getKey()));
if(b.getValue().is(ExpansionAnchorBlock.BLOCK))count++;
}
for(var site:p.sites){
if(Rosette183.sector(site.floor.getX(),site.floor.getZ())!=site.sector)throw new IllegalStateException("Wrong colour sector");
// Walk the full three-wide bent approach and verify its actual native support.
for(int step=-6;step<=11;step++){
if(step>=-1&&step<=1)continue;
int bend=step>3&&step<9?1:0;var at=site.floor.offset(site.dx*step-site.dz*bend,1,site.dz*step+site.dx*bend);
at=new BlockPos(at.getX(),pathY(source,site,at.getX(),at.getZ(),step)+1,at.getZ());
if(!level.getBlockState(at).isAir()||!level.getBlockState(at.above()).isAir()||level.getBlockState(at.below()).getCollisionShape(level,at.below()).isEmpty())throw new IllegalStateException("Anchor approach blocked "+at);
}
if(!rock(source,site.floor.getX(),site.floor.getY()-5,site.floor.getZ()))throw new IllegalStateException("Anchor above void");
if(site.dx*site.floor.getX()+site.dz*site.floor.getZ()>=0)throw new IllegalStateException("Approach points outward");
for(int side:new int[]{-1,1}){
var pylon=pylon(source,site,side);
if(!level.getBlockState(pylon.above(3)).is(Blocks.AMETHYST_BLOCK)||!level.getBlockState(pylon.above(4)).is(Rosette183.GLASS[site.sector]))throw new IllegalStateException("Pylon glass must sit on amethyst");
}
details.add(Map.of("sector",site.sector,"kind",site.kind.name(),"anchor",site.anchor().toShortString(),"entrance",site.entrance().toShortString(),"bright",site.bright));
}
if(count!=8||p.sites.stream().map(Site::sector).distinct().count()!=8)throw new IllegalStateException("Expected exactly eight anchors");
var offers=AnchorOffers189.check(level,p.sites);
return Map.of("sites",details,"planningMs",p.millis,"chunks",p.chunks.size(),"blocks",p.chunks.values().stream().mapToInt(Map::size).sum(),"deterministic",true,"offers",offers);
}
}
@@ -0,0 +1,32 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent184 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Palace184.Plan> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent184() {}
private static Palace184.Plan plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace184.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.ascent184(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);Sulfur183.place(source,chunk);NaturalWater184.place(source,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).blocks().forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("palace",Palace184.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("sulfur",Sulfur183.check(level,source));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,34 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent185 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Map<BlockPos,BlockState>> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent185() {}
private static Map<BlockPos,BlockState> plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace185.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.stem185(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);Sulfur183.place(source,chunk);NaturalWater184.place(source,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("palace",Palace185.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("sulfur",Sulfur183.check(level,source));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,35 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent186 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Map<BlockPos,BlockState>> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent186() {}
private static Map<BlockPos,BlockState> plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace186.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.anchors186(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);Sulfur183.place(source,chunk);NaturalWater184.place(source,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("anchors",Anchors186.check(level,source));
result.put("palace",Palace186.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("sulfur",Sulfur183.check(level,source));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent187 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Map<BlockPos,BlockState>> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent187() {}
private static Map<BlockPos,BlockState> plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace186.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.natural187(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);NaturalWater184.place(source,chunk);SurfaceWater187.place(source,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("anchors",Anchors187.check(level,source));
result.put("palace",Palace186.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("surfaceWater",SurfaceWater187.check(level,source));
result.put("sulfur",Sulfur187.check(level,source));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent188 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Map<BlockPos,BlockState>> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent188() {}
private static Map<BlockPos,BlockState> plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace186.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.details188(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);NaturalWater184.place(source,chunk);SurfaceWater187.place(source,chunk);Lakes188.decorate(source,chunk);SulfurDetails188.place(level,generator,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("anchors",Anchors188.check(level,source));
result.put("palace",Palace186.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("surfaceWater",SurfaceWater187.check(level,source));
result.put("sulfur",Sulfur187.check(level,source));result.put("sulfurFeatures",SulfurDetails188.check(level,source));result.put("lakeBeds",Lakes188.check(level,source));result.put("ironLoot",IronLoot188.check(level));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,37 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** New world only: retained island, native cavity water and natural vertical palace. */
public final class Ascent189 {
private record Key(long seed,Station183.Layout layout) {}
private static final Map<Key,Map<BlockPos,BlockState>> PLANS=new java.util.concurrent.ConcurrentHashMap<>();
private Ascent189() {}
private static Map<BlockPos,BlockState> plan(long seed,ChunkGenerator g){return PLANS.computeIfAbsent(new Key(seed,Station183.layout(g)),k->Palace186.plan(k.seed(),k.layout()));}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.finishedTerrain(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);NaturalWater184.place(source,chunk);Water189.finish(source,chunk);Gallery189.place(source,chunk);Ruins189.place(source,chunk);SulfurDetails188.place(level,generator,chunk);
var pos=chunk.getPos();
if(pos.x()>=-3&&pos.x()<=2&&pos.z()>=-3&&pos.z()<=2)
plan(level.getSeed(),generator).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
PalaceAtlas184.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var generator=level.getChunkSource().getGenerator();var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
var result=new LinkedHashMap<String,Object>();result.put(source.field().eroded()?"erodedMassif":source.originalRelief()?"originalRelief":source.field().refined()?"naturalRelief":"terraces",Terrain189.check(level,source));
if(source.field().coherent())result.put("continuity",Continuity193.check(level,source));
result.put("anchors",Anchors189.check(level,source));
result.put("palace",Palace186.check(level,plan(level.getSeed(),generator)));
result.put("naturalWater",NaturalWater184.check(level,source));
result.put("atlas",PalaceAtlas184.check(level));result.put("gallery",Cavern183.check(level));
result.put("surfaceWater",Water189.check(level,source));
result.put("sulfur",Sulfur187.check(level,source));result.put("sulfurFeatures",SulfurDetails188.check(level,source));result.put("galleryDetails",Gallery189.check(level,source));result.put("ruins",Ruins189.check(level,source));result.put("ironLoot",IronLoot188.check(level));result.put("gemDistricts",Gems183.check(level));return result;
}
}
@@ -0,0 +1,28 @@
package fr.koka.sanctuarytest;
import java.util.*;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.phys.AABB;
import org.joml.Matrix4f;
/** Verify the actual client frustum hooks once per new lab level, including both terrain APIs. */
final class AscentView184 {
private static final Set<ClientLevel> CHECKED=Collections.newSetFromMap(new WeakHashMap<>());
static void register(){ClientTickEvents.END_CLIENT_TICK.register(client->{
var level=client.level;
if(level==null||!level.dimensionTypeRegistration().is(Identifier.fromNamespaceAndPath("sanctuary_test","ascent_1600"))||!CHECKED.add(level))return;
var frustum=new Frustum(new Matrix4f().scaling(1/4096f),new Matrix4f());
var crown=new AABB(-32,1152,-32,32,1328,32);var root=new AABB(-16,624,-16,16,672,16);
var cube=new BoundingBox(-32,1152,-32,32,1328,32);
frustum.prepare(0,250,0);
if(frustum.isVisible(crown)||frustum.cubeInFrustum(cube)<0||!frustum.isVisible(root))throw new IllegalStateException("Ascent ground reveal regression");
frustum.prepare(0,640,0);
if(!frustum.isVisible(crown)||frustum.cubeInFrustum(cube)>=0)throw new IllegalStateException("Ascent crown remains hidden at rosette");
SanctuaryMod.LOGGER.info("ASCENT_VIEW_VERIFIED groundHidden=true rosetteVisible=true crownRevealedAt640=true");
});}
}
@@ -27,6 +27,7 @@ public final class Basins180 {
int bx=x+dx*u-dz*v,bz=z+dz*u+dx*v;
if(solid(s,bx,y-8,bz)&&!solid(s,bx,y+6,bz)&&s.terrain(bx,bz).top()>y+14)score++;
}
if(s.field().coherent()&&galleryOverlap(s,x,y+2,z,dx,dz))continue;
candidates.add(new Basin(x,y+2,z,dx,dz,score));
}
break;
@@ -36,9 +37,18 @@ public final class Basins180 {
for(var b:candidates)if(b.score()>=24&&chosen.stream().allMatch(a->Math.hypot(a.x()-b.x(),a.z()-b.z())>145)){
chosen.add(b);if(chosen.size()==2)break;
}
if(chosen.size()!=2)throw new IllegalStateException("No two retained cave lake sites: "+chosen+" candidates="+candidates.size());
if(chosen.isEmpty()||(!s.field().coherent()&&chosen.size()!=2))throw new IllegalStateException("No two retained cave lake sites: "+chosen+" candidates="+candidates.size());
return List.copyOf(chosen);
}
private static boolean galleryOverlap(AdventureBiomes180 s,int x,int y,int z,int dx,int dz){
if(Math.hypot(x,z)>135)return false;
for(var c:Cavern183.plan(s).cells().values()){
if(c.y()>y+4||c.ceiling()<y-16)continue;
int u=(c.x()-x)*dx+(c.z()-z)*dz,v=-(c.x()-x)*dz+(c.z()-z)*dx;
if(u>=-34&&u<=60&&Math.abs(v)<=26)return true;
}
return false;
}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess c){
if(!WorldgenLab.ecology180(generator))return;var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
for(var b:sites(source)){
@@ -0,0 +1,157 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Local, seed-bound rock gallery. All carving belongs to new gallery_v1 chunks. */
public final class Cavern182 {
record Room(int x,int y,int z,int rx,int rz) {}
record Cell(int x,int y,int z,int ceiling,boolean path) {}
record Plan(List<Room> rooms,Map<Long,Cell> cells,List<BlockPos> route) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Cavern182() {}
private static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
static Plan plan(AdventureBiomes180 source){synchronized(PLANS){return PLANS.computeIfAbsent(source,Cavern182::create);}}
private static int floor(AdventureBiomes180 s,int x,int z,int low,int high,int fallback) {
int best=-1,bestScore=Integer.MIN_VALUE;
for(int y=high;y>=low;y--) {
if(!rock(s,x,y,z)||!rock(s,x,y-3,z)||rock(s,x,y+1,z))continue;
boolean roof=false;for(int h=12;h<=40;h+=4)if(rock(s,x,y+h,z)){roof=true;break;}
if(!roof)continue;
int score=0;
for(int dx=-8;dx<=8;dx+=4)for(int dz=-8;dz<=8;dz+=4)
if(!rock(s,x+dx,y+4,z+dz)&&rock(s,x+dx,y-4,z+dz))score+=3;
score-=Math.abs(y-fallback)/3;
if(score>bestScore){best=y;bestScore=score;}
}
if(best>=0)return best;
if(fallback>0&&rock(s,x,fallback-3,z))return fallback;
throw new IllegalStateException("No supported central gallery floor at "+x+","+z);
}
private static Plan create(AdventureBiomes180 s) {
int y=floor(s,0,0,148,212,-1);
var rooms=List.of(new Room(0,y,0,14,11),
new Room(-34,floor(s,-34,-26,y-18,y+8,y-6),-26,10,8),
new Room(-38,floor(s,-38,24,y-30,y+8,y-16),24,12,10));
var cells=new LinkedHashMap<Long,Cell>();
for(var r:rooms)for(int x=r.x-r.rx-1;x<=r.x+r.rx+1;x++)for(int z=r.z-r.rz-1;z<=r.z+r.rz+1;z++) {
double u=(x-r.x)/(double)r.rx,v=(z-r.z)/(double)r.rz;
double d=u*u+v*v+.07*Math.sin(x*.6)*Math.cos(z*.4);
if(d>1)continue;
double amplitude=Math.clamp((Math.hypot(x-r.x,z-r.z)-5)/4,0,1);
int ground=r.y+(int)Math.round(amplitude*(Math.sin(x*.19)*1.1+Math.sin(z*.23)));
int ceiling=r.y+5+(int)(9*Math.sqrt(Math.max(0,1-d)));
cells.put(key(x,z),new Cell(x,ground,z,ceiling,false));
}
var route=new ArrayList<BlockPos>();
link(cells,route,rooms.get(0),rooms.get(1),new int[][]{{0,0},{-8,-12},{-23,-17},{-34,-26}});
link(cells,route,rooms.get(1),rooms.get(2),new int[][]{{-34,-26},{-49,-16},{-48,5},{-38,24}});
link(cells,route,rooms.get(2),rooms.get(0),new int[][]{{-38,24},{-25,20},{-12,8},{0,0}});
return new Plan(rooms,Map.copyOf(cells),List.copyOf(route));
}
private static void link(Map<Long,Cell> cells,List<BlockPos> route,Room a,Room b,int[][] turns) {
var line=new ArrayList<int[]>();
for(int i=1;i<turns.length;i++) {
int x=turns[i-1][0],z=turns[i-1][1],tx=turns[i][0],tz=turns[i][1];
int dx=Math.abs(tx-x),dz=Math.abs(tz-z),sx=Integer.signum(tx-x),sz=Integer.signum(tz-z),err=dx-dz;
while(x!=tx||z!=tz){line.add(new int[]{x,z});int twice=2*err;if(twice> -dz){err-=dz;x+=sx;}if(twice<dx){err+=dx;z+=sz;}}
}
line.add(new int[]{b.x,b.z});
for(int i=0;i<line.size();i++) {
var p=line.get(i);int y=(int)Math.round(a.y+(b.y-a.y)*i/(double)(line.size()-1));
route.add(new BlockPos(p[0],y+1,p[1]));
for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++) {
if(dx*dx+dz*dz>5)continue;
int x=p[0]+dx,z=p[1]+dz;var old=cells.get(key(x,z));
// Keep the first corridor floor at self-overlaps, guaranteeing a stable shared seam.
if(old!=null&&old.path)continue;
int roof=Math.max(old==null?y+6:old.ceiling,y+5+(dx==0||dz==0?2:0));
cells.put(key(x,z),new Cell(x,y,z,roof,true));
}
}
}
private static void set(ChunkAccess c,BlockPos p,Block b){c.setBlockState(p,b.defaultBlockState());}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk) {
if(!WorldgenLab.gallery182(generator))return;
if(chunk.getPos().getMaxBlockX()< -54||chunk.getPos().getMinBlockX()>15
||chunk.getPos().getMaxBlockZ()< -36||chunk.getPos().getMinBlockZ()>35)return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();var p=plan(source);
for(var c:p.cells.values()) {
var at=new BlockPos(c.x,c.y,c.z);if(!chunk.getPos().contains(at))continue;
for(int y=c.y-2;y<=c.ceiling;y++) {
var pos=new BlockPos(c.x,y,c.z);
if(y>c.y){set(chunk,pos,Blocks.AIR);continue;}
// Sculpt only this floor's local foundation, retaining minerals elsewhere.
Block stone=y<c.y?Blocks.TUFF:Math.floorMod(c.x*11+c.z*7,19)<3?Blocks.MOSS_BLOCK:Blocks.ANDESITE;
if(Math.abs(c.x)<=2&&Math.abs(c.z)<=2)stone=Blocks.POLISHED_TUFF;
set(chunk,pos,stone);
}
// Vertical seams colour existing stone; there is no box-shaped lining.
for(int[] d:new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
int wx=c.x+d[0],wz=c.z+d[1];if(p.cells.containsKey(key(wx,wz)))continue;
for(int y=c.y+1;y<c.ceiling;y++) {
var wall=new BlockPos(wx,y,wz);if(!chunk.getPos().contains(wall))continue;
if(!AdventureCaves180.natural(chunk.getBlockState(wall)))continue;
if(Math.floorMod(wx*3+wz*5+y/4,13)<2)set(chunk,wall,Blocks.CALCITE);
else if(y==c.y+1&&Math.floorMod(wx+wz,4)==0)set(chunk,wall,Blocks.MOSS_BLOCK);
else if(y==c.y+3&&Math.floorMod(wx+wz,7)==0)set(chunk,wall,Blocks.CHISELED_TUFF);
}
}
}
int y=p.rooms.getFirst().y;
// The dark core is a visual placeholder; the broken stone arch is not a Nether frame.
putIfOwned(chunk,0,y+1,0,Blocks.CHISELED_POLISHED_BLACKSTONE);
putIfOwned(chunk,0,y+2,0,Blocks.OBSIDIAN);
for(int side:new int[]{-1,1})for(int h=0;h<7;h++) {
int x=side*(4-(h>=5?1:0));
putIfOwned(chunk,x,y+1+h,-5,h%3==0?Blocks.OBSIDIAN:Blocks.POLISHED_BASALT);
}
for(int[] b:new int[][]{{-6,-4},{6,-4},{-5,5},{5,5}}) {
var cell=p.cells.get(key(b[0],b[1]));
putIfOwned(chunk,b[0],cell.y-1,b[1],Blocks.SEA_LANTERN);
putIfOwned(chunk,b[0],cell.y,b[1],Blocks.COPPER_GRATE.waxed().oxidized());
}
}
private static void putIfOwned(ChunkAccess chunk,int x,int y,int z,Block b) {
var p=new BlockPos(x,y,z);if(chunk.getPos().contains(p))set(chunk,p,b);
}
static Map<String,Object> check(ServerLevel level) {
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();var p=plan(source);
for(int x=-4;x<=0;x++)for(int z=-3;z<=2;z++)level.getChunk(x,z);
int cy=p.rooms.getFirst().y;
if(!level.getBlockState(new BlockPos(0,cy+2,0)).is(Blocks.OBSIDIAN))throw new IllegalStateException("Missing lower core");
// Follow the actual saved surfaces across chunk boundaries, not just the planned centreline.
var floors=new HashMap<Long,Integer>();
for(var c:p.cells.values()) {
if(Math.abs(c.x)<=1&&Math.abs(c.z)<=1)continue;
int actual=Integer.MIN_VALUE;
for(int y=c.y+2;y>=c.y-2;y--) {
var at=new BlockPos(c.x,y,c.z);
if(!level.getBlockState(at).getCollisionShape(level,at).isEmpty()&&level.getBlockState(at.above()).isAir()&&level.getBlockState(at.above(2)).isAir()){actual=y;break;}
}
if(actual!=Integer.MIN_VALUE)floors.put(key(c.x,c.z),actual);
}
long start=key(2,0);if(!floors.containsKey(start))throw new IllegalStateException("No gallery approach");
var visited=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(start);
while(!queue.isEmpty()) {
long k=queue.removeFirst();if(!visited.add(k))continue;int x=(int)(k>>32),z=(int)k;
for(int[] d:new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
long next=key(x+d[0],z+d[1]);var y=floors.get(next);
if(y!=null&&Math.abs(y-floors.get(k))<=1&&!visited.contains(next))queue.add(next);
}
}
for(var r:p.rooms)if(!visited.contains(key(r.x+2,r.z)))throw new IllegalStateException("Disconnected gallery room: "+r);
if(visited.size()<floors.size()*.9)throw new IllegalStateException("Too many isolated gallery surfaces: "+visited.size()+"/"+floors.size());
return Map.of("lowerCore",new BlockPos(0,cy+2,0).toShortString(),"rooms",p.rooms,"walkableCells",visited.size(),"plannedColumns",p.cells.size());
}
}
@@ -0,0 +1,214 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Local, seed-bound rock gallery. All carving belongs to new discovery_v1 chunks. */
public final class Cavern183 {
record Room(int x,int y,int z,int rx,int rz) {}
record Cell(int x,int y,int z,int ceiling,boolean path) {}
record Plan(List<Room> rooms,Map<Long,Cell> cells,List<BlockPos> route) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private Cavern183() {}
private static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
static Plan plan(AdventureBiomes180 source){synchronized(PLANS){return PLANS.computeIfAbsent(source,Cavern183::create);}}
private static int floor(AdventureBiomes180 s,int x,int z,int low,int high,int fallback) {
int best=-1,bestScore=Integer.MIN_VALUE;
for(int y=high;y>=low;y--) {
if(!rock(s,x,y,z)||!rock(s,x,y-3,z)||rock(s,x,y+1,z))continue;
boolean roof=false;for(int h=12;h<=40;h+=4)if(rock(s,x,y+h,z)){roof=true;break;}
if(!roof)continue;
int score=0;
for(int dx=-8;dx<=8;dx+=4)for(int dz=-8;dz<=8;dz+=4)
if(!rock(s,x+dx,y+4,z+dz)&&rock(s,x+dx,y-4,z+dz))score+=3;
score-=Math.abs(y-fallback)/3;
if(score>bestScore){best=y;bestScore=score;}
}
if(best>=0)return best;
if(fallback>0&&rock(s,x,fallback-3,z))return fallback;
return -1;
}
private static Room satellite(AdventureBiomes180 s,int x,int z,int centre,int preferred,int rx,int rz) {
Room best=null;int score=Integer.MAX_VALUE;
// Search only nearby existing cave floors, rather than forcing a floating room at a fixed coordinate.
for(int dx=-28;dx<=28;dx+=4)for(int dz=-28;dz<=28;dz+=4){
if(Math.hypot(x+dx,z+dz)>58||(z<0?z+dz> -12:z+dz<12))continue;
double separation=Math.pow((x+dx)/(double)(19+rx+3),2)+Math.pow((z+dz)/(double)(16+rz+3),2);
if(separation<1)continue; // Separate floor levels must meet through a ramp, never overlapping rooms.
int y=floor(s,x+dx,z+dz,centre-18,centre+8,-1);if(y<0)continue;
int cost=dx*dx+dz*dz+2*Math.abs(y-preferred);
if(cost<score){score=cost;best=new Room(x+dx,y,z+dz,rx,rz);}
}
return best; // Some seeds have no separate pocket here; retain the central chamber instead of forcing a room.
}
private static Plan create(AdventureBiomes180 s) {
int y=floor(s,0,0,148,212,-1);
// Some seeds have their central cave below the historical search band.
// Preserve every previously admitted floor; widen only a failed search.
if(y<0&&s.field().coherent())y=floor(s,0,0,48,240,-1);
// A seed can have solid rock at the origin without an existing cave floor.
// Keep admitted layouts identical; only sculpt a local chamber when needed.
if(y<0&&s.polish197())y=supportedChamber(s);
if(y<0&&s.stableGallery198())y=GalleryFloor198.choose((x,h,z)->rock(s,x,h,z));
if(y<0)throw new IllegalStateException("No supported central gallery floor");
var rooms=new ArrayList<Room>();rooms.add(new Room(0,y,0,19,16));
if(s.field().coherent()){
var north=satellite(s,-34,-26,y,y-6,10,8);if(north!=null)rooms.add(north);
var south=satellite(s,-38,24,y,y-16,12,10);if(south!=null)rooms.add(south);
}else{
rooms.add(new Room(-34,floor(s,-34,-26,y-18,y+8,y-6),-26,10,8));
rooms.add(new Room(-38,floor(s,-38,24,y-30,y+8,y-16),24,12,10));
}
for(var room:rooms)if(room.y<0)throw new IllegalStateException("No supported central gallery floor at "+room.x+","+room.z);
var cells=new LinkedHashMap<Long,Cell>();
for(var r:rooms)for(int x=r.x-r.rx-1;x<=r.x+r.rx+1;x++)for(int z=r.z-r.rz-1;z<=r.z+r.rz+1;z++) {
double u=(x-r.x)/(double)r.rx,v=(z-r.z)/(double)r.rz;
double d=u*u+v*v+.07*Math.sin(x*.6)*Math.cos(z*.4);
if(d>1)continue;
double amplitude=Math.clamp((Math.hypot(x-r.x,z-r.z)-5)/4,0,1);
int ground=r.y+(int)Math.round(amplitude*(Math.sin(x*.19)*1.1+Math.sin(z*.23)));
int ceiling=r.y+5+(int)(13*Math.sqrt(Math.max(0,1-d)));
cells.put(key(x,z),new Cell(x,ground,z,ceiling,false));
}
var route=new ArrayList<BlockPos>();
if(rooms.size()==3){
var a=rooms.get(1);var b=rooms.get(2);
link(cells,route,rooms.get(0),a,new int[][]{{0,0},{-8,-12},{-23,-17},{a.x,a.z}});
link(cells,route,a,b,new int[][]{{a.x,a.z},{-49,-16},{-48,5},{b.x,b.z}});
link(cells,route,b,rooms.get(0),new int[][]{{b.x,b.z},{-25,20},{-12,8},{0,0}});
}else if(rooms.size()==2){
var a=rooms.get(1);link(cells,route,rooms.getFirst(),a,new int[][]{{0,0},{a.x/2,a.z/2},{a.x,a.z}});
}
return new Plan(List.copyOf(rooms),Map.copyOf(cells),List.copyOf(route));
}
private static int supportedChamber(AdventureBiomes180 s) {
int best=-1,bestScore=Integer.MIN_VALUE,top=s.field().surface(0,0);
for(int y=32;y<=Math.min(240,top-22);y++) {
if(!rock(s,0,y,0)||!rock(s,0,y-3,0))continue;
int score=-Math.abs(y-164)/3;
for(int x=-16;x<=16;x+=8)for(int z=-12;z<=12;z+=6) {
if(rock(s,x,y-3,z))score+=4;
if(rock(s,x,y+20,z))score+=2;
if(!rock(s,x,y+5,z))score++;
}
if(score>bestScore){bestScore=score;best=y;}
}
return best;
}
private static void link(Map<Long,Cell> cells,List<BlockPos> route,Room a,Room b,int[][] turns) {
var line=new ArrayList<int[]>();
for(int i=1;i<turns.length;i++) {
int x=turns[i-1][0],z=turns[i-1][1],tx=turns[i][0],tz=turns[i][1];
int dx=Math.abs(tx-x),dz=Math.abs(tz-z),sx=Integer.signum(tx-x),sz=Integer.signum(tz-z),err=dx-dz;
while(x!=tx||z!=tz){line.add(new int[]{x,z});int twice=2*err;if(twice> -dz){err-=dz;x+=sx;}if(twice<dx){err+=dx;z+=sz;}}
}
line.add(new int[]{b.x,b.z});
for(int i=0;i<line.size();i++) {
var p=line.get(i);int y=(int)Math.round(a.y+(b.y-a.y)*i/(double)(line.size()-1));
route.add(new BlockPos(p[0],y+1,p[1]));
for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++) {
if(dx*dx+dz*dz>5)continue;
int x=p[0]+dx,z=p[1]+dz;var old=cells.get(key(x,z));
// Keep the first corridor floor at self-overlaps, guaranteeing a stable shared seam.
if(old!=null&&old.path)continue;
int roof=Math.max(old==null?y+6:old.ceiling,y+5+(dx==0||dz==0?2:0));
cells.put(key(x,z),new Cell(x,y,z,roof,true));
}
}
}
private static void set(ChunkAccess c,BlockPos p,Block b){c.setBlockState(p,b.defaultBlockState());}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk) {
if(!WorldgenLab.discovery183(generator)&&!WorldgenLab.ascent184(generator)&&!WorldgenLab.stem185(generator)&&!WorldgenLab.anchors186(generator)&&!WorldgenLab.natural187(generator)&&!WorldgenLab.details188(generator)&&!WorldgenLab.finishedTerrain(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
int margin=source.field().coherent()?32:0;
if(chunk.getPos().getMaxBlockX()< -54-margin||chunk.getPos().getMinBlockX()>20
||chunk.getPos().getMaxBlockZ()< -36-margin||chunk.getPos().getMinBlockZ()>35+margin)return;
var p=plan(source);
for(var c:p.cells.values()) {
var at=new BlockPos(c.x,c.y,c.z);if(!chunk.getPos().contains(at))continue;
for(int y=c.y-2;y<=c.ceiling;y++) {
var pos=new BlockPos(c.x,y,c.z);
if(y>c.y){set(chunk,pos,Blocks.AIR);continue;}
// Sculpt only this floor's local foundation, retaining minerals elsewhere.
Block stone=y<c.y?Blocks.TUFF:Math.floorMod(c.x*11+c.z*7,19)<3?Blocks.MOSS_BLOCK:Blocks.ANDESITE;
if(Math.abs(c.x)<=2&&Math.abs(c.z)<=2)stone=Blocks.POLISHED_TUFF;
set(chunk,pos,stone);
}
// Vertical seams colour existing stone; there is no box-shaped lining.
for(int[] d:new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
int wx=c.x+d[0],wz=c.z+d[1];if(p.cells.containsKey(key(wx,wz)))continue;
for(int y=c.y+1;y<c.ceiling;y++) {
var wall=new BlockPos(wx,y,wz);if(!chunk.getPos().contains(wall))continue;
if(!AdventureCaves180.natural(chunk.getBlockState(wall)))continue;
if(Math.floorMod(wx*3+wz*5+y/4,13)<2)set(chunk,wall,Blocks.CALCITE);
else if(y==c.y+1&&Math.floorMod(wx+wz,4)==0)set(chunk,wall,Blocks.MOSS_BLOCK);
else if(y==c.y+3&&Math.floorMod(wx+wz,7)==0)set(chunk,wall,Blocks.CHISELED_TUFF);
}
}
}
int y=p.rooms.getFirst().y;
// Horizontal 5 x 5 rim, dry 3 x 3 recess exactly one block below the walking plane.
for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++) {
boolean rim=Math.abs(x)==2||Math.abs(z)==2;
putIfOwned(chunk,x,y,z,rim?Blocks.OBSIDIAN:Blocks.AIR);
putIfOwned(chunk,x,y-1,z,Blocks.POLISHED_DEEPSLATE);
putIfOwned(chunk,x,y+1,z,Blocks.AIR);
putIfOwned(chunk,x,y+2,z,Blocks.AIR);
}
for(int[] b:new int[][]{{-6,-4},{6,-4},{-5,5},{5,5}}) {
var cell=p.cells.get(key(b[0],b[1]));
putIfOwned(chunk,b[0],cell.y-1,b[1],Blocks.SEA_LANTERN);
putIfOwned(chunk,b[0],cell.y,b[1],Blocks.COPPER_GRATE.waxed().oxidized());
}
}
private static void putIfOwned(ChunkAccess chunk,int x,int y,int z,Block b) {
var p=new BlockPos(x,y,z);if(chunk.getPos().contains(p))set(chunk,p,b);
}
static Map<String,Object> check(ServerLevel level) {
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();var p=plan(source);
var chunks=new HashSet<net.minecraft.world.level.ChunkPos>();
for(var c:p.cells.values())chunks.add(new net.minecraft.world.level.ChunkPos(c.x>>4,c.z>>4));
for(var cp:chunks)level.getChunk(cp.x(),cp.z());
int cy=p.rooms.getFirst().y;
for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++) {
boolean rim=Math.abs(x)==2||Math.abs(z)==2;var at=new BlockPos(x,cy,z);
if(!level.getBlockState(at).is(rim?Blocks.OBSIDIAN:Blocks.AIR)
||!level.getBlockState(at.below()).is(Blocks.POLISHED_DEEPSLATE)
||!level.getBlockState(at.above()).isAir()||!level.getBlockState(at.above(2)).isAir())
throw new IllegalStateException("Incorrect horizontal portal at "+at);
}
// Follow the actual saved surfaces across chunk boundaries, not just the planned centreline.
var floors=new HashMap<Long,Integer>();
for(var c:p.cells.values()) {
if(Math.abs(c.x)<=1&&Math.abs(c.z)<=1)continue;
int actual=Integer.MIN_VALUE;
for(int y=c.y+2;y>=c.y-2;y--) {
var at=new BlockPos(c.x,y,c.z);
if(!level.getBlockState(at).getCollisionShape(level,at).isEmpty()&&level.getBlockState(at.above()).isAir()&&level.getBlockState(at.above(2)).isAir()){actual=y;break;}
}
if(actual!=Integer.MIN_VALUE)floors.put(key(c.x,c.z),actual);
}
long start=key(2,0);if(!floors.containsKey(start))throw new IllegalStateException("No gallery approach");
var visited=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(start);
while(!queue.isEmpty()) {
long k=queue.removeFirst();if(!visited.add(k))continue;int x=(int)(k>>32),z=(int)k;
for(int[] d:new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
long next=key(x+d[0],z+d[1]);var y=floors.get(next);
if(y!=null&&Math.abs(y-floors.get(k))<=1&&!visited.contains(next))queue.add(next);
}
}
for(var r:p.rooms)if(!visited.contains(key(r.x+2,r.z)))throw new IllegalStateException("Disconnected gallery room: "+r);
if(visited.size()<floors.size()*.9)throw new IllegalStateException("Too many isolated gallery surfaces: "+visited.size()+"/"+floors.size());
return Map.of("horizontalPortal",new BlockPos(0,cy,0).toShortString(),"rooms",p.rooms,"walkableCells",visited.size(),"plannedColumns",p.cells.size());
}
}
@@ -15,6 +15,9 @@ public final class Cherry180 {
private static final Map<AdventureBiomes180,Map<Long,BlockPos>> PLACED=Collections.synchronizedMap(new WeakHashMap<>());
private Cherry180(){}
public static List<BlockPos> sites(AdventureBiomes180 source){synchronized(PLANS){return PLANS.computeIfAbsent(source,s->{
if(s.field().coherent())return coherentSites(s);
if(s.field().eroded())return s.field().erosion().crowns().stream()
.map(c->new BlockPos(c.x(),s.field().erosion().surface(c.x(),c.z()),c.z())).toList();
var candidates=new ArrayList<BlockPos>();int cx=s.field().peakX()>>4,cz=s.field().peakZ()>>4;
for(int dx=-4;dx<=4;dx++)for(int dz=-4;dz<=4;dz++){
int x=(cx+dx)*16+8,z=(cz+dz)*16+8,y=s.terrain(x,z).top();if(y>=270)candidates.add(new BlockPos(x,y,z));
@@ -23,15 +26,49 @@ public final class Cherry180 {
for(var p:candidates)if(chosen.stream().allMatch(q->q.distSqr(p)>24*24)){chosen.add(p);if(chosen.size()==3)break;}
if(chosen.size()!=3)throw new IllegalStateException("No three spacious summit sites");return List.copyOf(chosen);
});}}
private static List<BlockPos> coherentSites(AdventureBiomes180 s){
var chosen=new ArrayList<BlockPos>();var water=Water189.plan(s).columns();
for(var crown:s.field().erosion().crowns()){
var candidates=new ArrayList<BlockPos>();
for(int dx=-40;dx<=40;dx+=2)for(int dz=-40;dz<=40;dz+=2){
int x=crown.x()+dx,z=crown.z()+dz,y=s.field().erosion().surface(x,z);
if(y<250||s.field().erosion().slope(x,z)>.75||Math.abs(y-crown.height())>14)continue;
var p=new BlockPos(x,y,z);
if(chosen.stream().anyMatch(q->(q.getX()>>4)==(x>>4)&&(q.getZ()>>4)==(z>>4)||q.distSqr(p)<24*24))continue;
if(Anchors189.treeBlocked(s,p.above()))continue;
boolean clear=true;
for(int ox=-5;ox<=5&&clear;ox++)for(int oz=-5;oz<=5&&clear;oz++){
int top=water.getOrDefault(((long)(x+ox)<<32)^((z+oz)&0xffffffffL),-1);
if(top>=0&&y>=top-30&&y<=top+28)clear=false;
}
for(int ox=-4;ox<=4&&clear;ox+=4)for(int oz=-4;oz<=4&&clear;oz+=4)
for(int h=3;h<=11;h+=2)if(s.field().sampleValue(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.EMPTY_UNCACHED,x+ox,y+h,z+oz)>0)clear=false;
if(clear)candidates.add(p);
}
candidates.sort(Comparator.comparingDouble(p->p.distSqr(new BlockPos(crown.x(),(int)crown.height(),crown.z()))));
if(candidates.isEmpty())throw new IllegalStateException("No dry, open summit tree site near "+crown);
chosen.add(candidates.getFirst());
}
return List.copyOf(chosen);
}
public static List<BlockPos> trees(AdventureBiomes180 source){synchronized(PLACED){return List.copyOf(PLACED.getOrDefault(source,Map.of()).values());}}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess c){
if(!WorldgenLab.ecology180(generator))return;var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
if(sites(source).stream().noneMatch(c.getPos()::contains))return;
var random=RandomSource.create(level.getSeed()^c.getPos().pack()^0x180C4EL);
var feature=level.registryAccess().lookupOrThrow(Registries.FEATURE).getValue(Identifier.withDefaultNamespace("cherry_bees_005"));
for(int attempt=0;attempt<49;attempt++){
int x=c.getPos().getMinBlockX()+5+attempt%7,z=c.getPos().getMinBlockZ()+5+attempt/7;
for(int y=315;y>=270;y--){
var positions=new ArrayList<BlockPos>();
if(source.field().eroded()){
var center=sites(source).stream().filter(c.getPos()::contains).findFirst().orElseThrow();
for(int dx=0;dx<16;dx++)for(int dz=0;dz<16;dz++)
positions.add(new BlockPos(c.getPos().getMinBlockX()+dx,center.getY(),c.getPos().getMinBlockZ()+dz));
positions.sort(Comparator.comparingDouble(p->p.distSqr(center)));
}else for(int attempt=0;attempt<49;attempt++)
positions.add(new BlockPos(c.getPos().getMinBlockX()+5+attempt%7,0,c.getPos().getMinBlockZ()+5+attempt/7));
for(var candidate:positions){
int x=candidate.getX(),z=candidate.getZ();
if(source.field().eroded() && source.field().erosion().slope(x,z)>.75)continue;
for(int y=315;y>=(source.field().coherent()?250:270);y--){
var p=new BlockPos(x,y,z);if(!AdventureCaves180.natural(c.getBlockState(p))||!c.getBlockState(p.above()).isAir())continue;
c.setBlockState(p,Blocks.GRASS_BLOCK.defaultBlockState());
if(feature.place(level,generator,random,p.above())){synchronized(PLACED){PLACED.computeIfAbsent(source,s->new HashMap<>()).put(c.getPos().pack(),p.above());}return;}break;
@@ -0,0 +1,56 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Shared spatial checks for independent decorators; diagnostics run only in the laboratory. */
final class Continuity193 {
static boolean touches(Set<BlockPos> a,Set<BlockPos> b){
if(a.size()>b.size())return touches(b,a);
for(var p:a){if(b.contains(p))return true;for(var d:Direction.values())if(b.contains(p.relative(d)))return true;}
return false;
}
static boolean covered(ReliefEcology177.Field f,int x,int y,int z){
int roof=0;
for(int[] d:new int[][]{{0,0},{4,0},{-4,0},{0,4},{0,-4}}){
for(int dy=4;dy<=40;dy+=4)if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,x+d[0],y+dy,z+d[1])>0){roof++;break;}
}
return roof>=3;
}
record Water(String kind,BlockPos seed,int surface,Set<BlockPos> cells){}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var pools=new ArrayList<Water>();
for(var p:NaturalWater184.plan(s).pools())pools.add(new Water("cavity",p.seed(),p.surface(),p.cells()));
for(var p:SurfaceWater187.plan(s).pools())pools.add(new Water("surface",p.seed(),p.surface(),p.cells()));
int pairs=0,cells=0;var details=new ArrayList<Map<String,Object>>();
for(int i=0;i<pools.size();i++){
var p=pools.get(i);
for(int j=i+1;j<pools.size();j++){pairs++;if(touches(p.cells,pools.get(j).cells))throw new IllegalStateException("Independent water bodies touch: "+p.seed+" / "+pools.get(j).seed);}
var chunks=new HashSet<net.minecraft.world.level.ChunkPos>();
for(var q:p.cells)chunks.add(new net.minecraft.world.level.ChunkPos(q.getX()>>4,q.getZ()>>4));
for(var cp:chunks)level.getChunk(cp.x(),cp.z());
int empty=0;var missing=new TreeMap<String,Integer>();
for(var q:p.cells){cells++;if(level.getFluidState(q).isEmpty()){empty++;missing.merge(level.getBlockState(q).toString(),1,Integer::sum);}}
if(empty!=0)throw new IllegalStateException("Planned lake lost "+empty+" water cells at "+p.seed+" "+missing);
details.add(Map.of("kind",p.kind,"seed",p.seed.toShortString(),"surface",p.surface,"cells",p.cells.size()));
}
int naturalFloors=0,aerial=0;var anchors=Anchors189.plan(s,s.seed());
for(var site:anchors.sites())if(site.kind()!=Anchors189.Kind.CAVE){
if(site.kind()==Anchors189.Kind.SKY)aerial++;
var centre=site.floor();
for(var chunk:anchors.chunks().values())for(var e:chunk.entrySet()){
var p=e.getKey();if(Math.abs(p.getX()-centre.getX())>10||Math.abs(p.getZ()-centre.getZ())>10||Math.abs(p.getY()-centre.getY())>16)continue;
if(e.getValue().is(Blocks.MOSS_BLOCK))throw new IllegalStateException("Moss checkerboard in exposed anchor "+p);
if(e.getValue().is(Blocks.GRASS_BLOCK)){
var f=s.field();if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())<=0||f.sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY()+1,p.getZ())>0)throw new IllegalStateException("Anchor floor detached from density "+p);
naturalFloors++;
}
}
}
if(aerial==0||naturalFloors==0)throw new IllegalStateException("Missing supported aerial refuge");
return Map.of("waterBodies",details,"disjointPairsChecked",pairs,"waterCellsChecked",cells,"overlappingWaterBodies",0,"exposedAnchorMoss",0,"anchorFloorsOnNaturalSurface",naturalFloors,"aerialAnchors",aerial);
}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Local repairs for new beta.197 worlds. No mutation of historical profiles. */
public final class Details197 {
private Details197() {}
private static boolean rock(AdventureBiomes180 s,BlockPos p) {
return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())>0;
}
static boolean openCliff(AdventureBiomes180 s,BlockPos lip,Direction outward) {
// A void below a bank can be a cave, with soil above it. Require open sky
// and a clear outward face for the first eight blocks of the curtain.
for(int y=lip.getY()+3;y<=319;y++)
if(rock(s,new BlockPos(lip.getX(),y,lip.getZ())))return false;
for(int down=1;down<=8;down++)for(int forward=0;forward<=2;forward++)
if(rock(s,lip.below(down).relative(outward,forward)))return false;
return true;
}
public static BlockPos islandSpawn(ServerLevel level,int x,int z) {
for(int y=316;y>=198;y--) {
var floor=new BlockPos(x,y,z);var state=level.getBlockState(floor);
if(state.is(BlockTags.LEAVES)||state.is(BlockTags.LOGS)||!state.isSolidRender())continue;
if(level.getBlockState(floor.above()).isAir()&&level.getBlockState(floor.above(2)).isAir())return floor.above();
}
return null;
}
private static int roofHeight(int x) {return 8-Math.abs(x);}
static boolean hutFits(AdventureBiomes180 s,BlockPos p) {
for(int x=-4;x<=4;x++)for(int z=-5;z<=5;z++) {
int roof=roofHeight(x);
for(int y=2;y<=roof+2;y++)if(rock(s,p.offset(x,y,z)))return false;
}
for(int x:new int[]{-3,3})for(int z:new int[]{-4,4}) {
boolean support=false;
for(int depth=0;depth<=8;depth++)if(rock(s,p.offset(x,-depth,z))){support=true;break;}
if(!support)return false;
}
return true;
}
private static BlockState roof(int x) {
if(x==0)return Blocks.DARK_OAK_SLAB.defaultBlockState();
return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,x<0?Direction.EAST:Direction.WEST);
}
private static void put(ChunkAccess c,BlockPos p,BlockState state){c.setBlockState(p,state);}
static void hut(ChunkAccess c,BlockPos origin) {
var p=origin.above();
for(int x=-4;x<=4;x++)for(int z=-5;z<=5;z++) {
var floor=p.offset(x,0,z);if(!c.getPos().contains(floor))continue;
int h=roofHeight(x);boolean corner=Math.abs(x)==3&&Math.abs(z)==4;
for(int y=1;y<h;y++)put(c,floor.above(y),Blocks.AIR.defaultBlockState());
if(Math.abs(x)<=3&&Math.abs(z)<=4) {
put(c,floor,Blocks.SPRUCE_PLANKS.defaultBlockState());
if(Math.abs(x)==3||Math.abs(z)==4)for(int y=1;y<h;y++) {
if(z==-4&&x==0&&y<=2)continue;
put(c,floor.above(y),(corner?Blocks.SPRUCE_LOG:y==2&&z==0?Blocks.GLASS:Blocks.SPRUCE_PLANKS).defaultBlockState());
}
}
if(corner)for(int depth=1;depth<=9;depth++) {
var at=floor.below(depth);if(AdventureCaves180.natural(c.getBlockState(at)))break;
put(c,at,Blocks.SPRUCE_LOG.defaultBlockState());
}
put(c,floor.above(h),roof(x));
if(z==-5&&Math.abs(x)<=2)put(c,floor,Blocks.SPRUCE_PLANKS.defaultBlockState());
if(x==2&&z==2)put(c,floor.above(),Blocks.CAULDRON.defaultBlockState());
if(x==-2&&z==2)put(c,floor.above(),Blocks.CRAFTING_TABLE.defaultBlockState());
if(x==0&&z==3)put(c,floor.above(),Blocks.LANTERN.defaultBlockState());
}
}
public static boolean protectsHut(ChunkGenerator g,BlockPos p) {
if(!SharedIsland196.current(g)||p.getY()<80||p.getY()>240||Math.abs(p.getX())>230||Math.abs(p.getZ())>230)return false;
var s=(AdventureBiomes180)((NoiseBasedChunkGenerator)g).getBiomeSource();var hut=Dungeon180.plan(s).hut();
return hut!=null&&Math.abs(p.getX()-hut.getX())<=4&&Math.abs(p.getZ()-hut.getZ())<=5
&&p.getY()>=hut.getY()-8&&p.getY()<=hut.getY()+10;
}
static Map<String,Object> check(ServerLevel level) {
var s=(AdventureBiomes180)level.getChunkSource().getGenerator().getBiomeSource();
var result=new LinkedHashMap<String,Object>();var hut=Dungeon180.plan(s).hut();
if(hut!=null) {
int roofBlocks=0;
for(int x=-4;x<=4;x++)for(int z=-5;z<=5;z++) {
var p=hut.offset(x,1+roofHeight(x),z);level.getChunk(p.getX()>>4,p.getZ()>>4);
if(!level.getBlockState(p).equals(roof(x)))throw new IllegalStateException("Broken hut roof at "+p+": "+level.getBlockState(p));
roofBlocks++;
}
for(int y=2;y<=3;y++)if(!level.getBlockState(hut.offset(0,y,-4)).isAir())throw new IllegalStateException("Blocked hut entrance");
result.put("hut",hut.toShortString());result.put("intactRoofBlocks",roofBlocks);
}else result.put("hut","no supported clear marsh site");
int curtains=0;
for(var outlet:Water189.plan(s).outlets()) {
var d=Direction.getApproximateNearest(outlet.lip().getX()-outlet.start().getX(),0,outlet.lip().getZ()-outlet.start().getZ());
if(!openCliff(s,outlet.lip(),d))throw new IllegalStateException("Buried waterfall mouth "+outlet);
for(int h=1;h<=8;h++) {
var p=outlet.lip().below(h);
if(level.getFluidState(p).isEmpty())throw new IllegalStateException("Obstructed waterfall "+p);
if(level.getBlockState(p.relative(d)).isSolidRender())throw new IllegalStateException("Waterfall inside cliff "+p);
}
curtains++;
}
result.put("exteriorWaterfallLanes",curtains);return result;
}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** Entirely opt-in, chunk-owned discovery revision; no save migration or runtime rebuild. */
public final class Discovery183 {
private Discovery183() {}
private static final Map<Station183.Layout,Map<BlockPos,BlockState>> STATIONS=new java.util.concurrent.ConcurrentHashMap<>();
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.discovery183(generator))return;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Cavern183.place(level,generator,chunk);
Sulfur183.place(source,chunk);
Water183.place(source,chunk);
var p=chunk.getPos();
if(p.x()>=-1&&p.x()<=0&&p.z()>=-1&&p.z()<=0)
Rosette183.plan(level.getSeed()).forEach((at,b)->{if(p.contains(at))chunk.setBlockState(at,b);});
if(p.x()>=-3&&p.x()<=2&&p.z()>=-2&&p.z()<=1)
STATIONS.computeIfAbsent(Station183.layout(generator),Station183::plan).forEach((at,b)->{if(p.contains(at))chunk.setBlockState(at,b);});
StationAtlas183.populate(level,generator,chunk);
}
static Map<String,Object> check(ServerLevel level){
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();
var result=new LinkedHashMap<String,Object>();
result.put("gallery",Cavern183.check(level));result.put("rosette",Rosette183.check(level));
result.put("sulfur",Sulfur183.check(level,source));result.put("upperWater",Water183.check(level,source));
result.put("gemDistricts",Gems183.check(level));result.put("ISS",Station183.check(level));result.put("atlas",StationAtlas183.check(level));
return result;
}
}
@@ -24,6 +24,28 @@ public final class Dungeon180 {
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
public static final ResourceKey<LootTable> LOOT=ResourceKey.create(Registries.LOOT_TABLE,Identifier.fromNamespaceAndPath("sanctuary_test","chests/mine_cache_180"));
private Dungeon180(){}
private static final Map<AdventureBiomes180,Map<Long,Integer>> FLOORS=Collections.synchronizedMap(new WeakHashMap<>());
/** Native dripstone pools can replace a floor after this chunk's corridors were placed. */
public static boolean protectsFloor(ChunkGenerator generator,BlockPos pos){
if(SharedIsland196.size(generator)==null||pos.getY()<80||pos.getY()>230||Math.abs(pos.getZ())>180)return false;
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
Map<Long,Integer> floors;
synchronized(FLOORS){floors=FLOORS.computeIfAbsent(source,s->{
var p=plan(s);var m=new HashMap<Long,Integer>();
for(var cell:p.paths().values())for(int offset=-1;offset<=1;offset++){
int x=cell.x()+(cell.dz()!=0?offset:0),z=cell.z()+(cell.dx()!=0?offset:0);
m.putIfAbsent(key(x,z),cell.y());
}
// Centre lines and rooms own their floor even where the widened corridors overlap.
for(var cell:p.paths().values())m.put(key(cell.x(),cell.z()),cell.y());
for(var room:p.rooms())for(int x=room.x()-room.rx()-1;x<=room.x()+room.rx()+1;x++)
for(int z=room.z()-room.rz()-1;z<=room.z()+room.rz()+1;z++){
var owner=roomAt(p,x,z);if(owner!=null)m.put(key(x,z),owner.y());
}
return Map.copyOf(m);
});}
var y=floors.get(key(pos.getX(),pos.getZ()));return y!=null&&y==pos.getY();
}
static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
public static Plan plan(AdventureBiomes180 source){synchronized(PLANS){return PLANS.computeIfAbsent(source,Dungeon180::create);}}
@@ -43,7 +65,7 @@ public final class Dungeon180 {
var paths=new LinkedHashMap<Long,Cell>();
for(int i=0;i<edges.length;i++){var a=rooms.get(edges[i][0]);var b=rooms.get(edges[i][1]);link(paths,a.x(),a.y(),a.z(),b.x(),b.y(),b.z(),i%2==0);}
int exitX=origin.getX()+36,exitZ=origin.getZ()+20*mirror;
while(exitX<360&&s.terrain(exitX,exitZ).top()>origin.getY()+4)exitX+=4;
while(exitX<(int)(360*s.field().base().spread())&&s.terrain(exitX,exitZ).top()>origin.getY()+4)exitX+=4;
// Daylight access bends twice instead of forming one unbroken tunnel.
link(paths,origin.getX(),origin.getY(),origin.getZ(),origin.getX()+28,origin.getY(),exitZ,false);
link(paths,origin.getX()+28,origin.getY(),exitZ,exitX+3,origin.getY(),exitZ,true);
@@ -51,7 +73,7 @@ public final class Dungeon180 {
for(int x=-224;x<=224;x+=16)for(int z=-224;z<=224;z+=16)for(int y=Math.min(216,s.terrain(x,z).top()-20);y>=96;y-=2){
if(!AdventureCaves180.biome(s,x,y,z).equals("adventure_marsh")||!rock(s,x,y,z)||rock(s,x,y+1,z)||rock(s,x,y+8,z))continue;
int n=0;for(int a=-4;a<=4;a+=2)for(int b=-4;b<=4;b+=2)if(!rock(s,x+a,y+5,z+b)&&rock(s,x+a,y-3,z+b))n++;
if(n>best){best=n;hut=new BlockPos(x,y,z);}break;
if(n>best && (!s.polish197() || Details197.hutFits(s,new BlockPos(x,y,z)))){best=n;hut=new BlockPos(x,y,z);}break;
}
return new Plan(List.copyOf(rooms),Map.copyOf(paths),hut,exitX+3,exitZ);
}
@@ -90,7 +112,9 @@ public final class Dungeon180 {
double dist=Math.pow((x-r.x())/(double)r.rx(),2)+Math.pow((z-r.z())/(double)r.rz(),2);
int h=4+(int)(4*Math.sqrt(Math.max(0,1-dist)));
boolean ground=AdventureCaves180.natural(c.getBlockState(new BlockPos(x,r.y(),z)));
set(c,x,r.y(),z,ground?(Math.floorMod(x*31+z*17,7)==0?Blocks.COARSE_DIRT:Blocks.STONE):Blocks.DARK_OAK_PLANKS);
set(c,x,r.y(),z,ground?(source.polish197()
? (EcologyBiomes176.noise(source.seed()^0x197D17L,x/9.0,z/9.0)>.32?Blocks.COARSE_DIRT:Blocks.STONE)
: (Math.floorMod(x*31+z*17,7)==0?Blocks.COARSE_DIRT:Blocks.STONE)):Blocks.DARK_OAK_PLANKS);
for(int dy=1;dy<=h;dy++)set(c,x,r.y()+dy,z,Blocks.AIR);
if(r.kind()==1&&!c.getBlockState(new BlockPos(x,r.y()+h+1,z)).isSolidRender())set(c,x,r.y()+h+1,z,Blocks.MOSSY_COBBLESTONE);
if(r.kind()==2&&x==r.x()+3&&z==r.z()){
@@ -112,7 +136,7 @@ public final class Dungeon180 {
if(offset==0&&cell.step()%7!=0&&cell.dx()!=0)c.setBlockState(new BlockPos(x,y+1,z),Blocks.RAIL.defaultBlockState().setValue(RailBlock.SHAPE,RailShape.EAST_WEST));
}else if(frame){for(int dy=0;dy<=4;dy++)set(c,x,y+dy,z,Blocks.DARK_OAK_LOG);}
}
if(p.hut()!=null)hut(c,p.hut());
if(p.hut()!=null){if(source.polish197())Details197.hut(c,p.hut());else hut(c,p.hut());}
}
private static void hut(ChunkAccess c,BlockPos p){
for(int x=Math.max(c.getPos().getMinBlockX(),p.getX()-4);x<=Math.min(c.getPos().getMaxBlockX(),p.getX()+4);x++)for(int z=Math.max(c.getPos().getMinBlockZ(),p.getZ()-5);z<=Math.min(c.getPos().getMaxBlockZ(),p.getZ()+5);z++){
@@ -139,7 +163,7 @@ public final class Dungeon180 {
for(int dx=-1;dx<=1;dx++)if(c.getPos().contains(at.offset(dx,0,0)))c.setBlockState(at.offset(dx,0,0),Blocks.RAIL.defaultBlockState().setValue(RailBlock.SHAPE,RailShape.EAST_WEST));
var cart=EntityTypes.CHEST_MINECART.create(level.getLevel(),EntitySpawnReason.STRUCTURE);
if(cart==null)throw new IllegalStateException("Missing chest minecart");
cart.snapTo(r.x()+.5,r.y()+1.0625,r.z()+.5,90,0);cart.setLootTable(LOOT,level.getSeed()^at.asLong());
cart.snapTo(r.x()+.5,r.y()+1.0625,r.z()+.5,90,0);cart.setLootTable((WorldgenLab.details188(generator)||WorldgenLab.finishedTerrain(generator))?ResourceKey.create(Registries.LOOT_TABLE,Identifier.fromNamespaceAndPath("sanctuary_test","chests/mine_cache_188")):LOOT,level.getSeed()^at.asLong());
cart.setCustomName(Component.translatable("entity.sanctuary_test.treasure_cart"));
cart.setUUID(UUID.nameUUIDFromBytes(("sanctuary_test:dungeon180:"+level.getSeed()+":"+r.id()).getBytes(StandardCharsets.UTF_8)));
level.addFreshEntity(cart);
@@ -0,0 +1,124 @@
package fr.koka.sanctuarytest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Bounded summit erosion over the original 3D field; no island-wide terraces or water solver. */
final class Erosion192 {
record Crown(int x,int z,double height,double dx,double dz){}
private final ReliefEcology177.Field original;
private final boolean coherent;
private final List<Crown> crowns;
private final Map<Long,Double> oldTops=new ConcurrentHashMap<>(),roofs=new ConcurrentHashMap<>();
private final Map<Long,Integer> surfaces=new ConcurrentHashMap<>();
private final Map<Long,Float> slopes=new ConcurrentHashMap<>();
Erosion192(ReliefEcology177.Field original,boolean coherent){
this.original=original;this.coherent=coherent;
var candidates=new ArrayList<int[]>();
for(int x=original.peakX()-64;x<=original.peakX()+64;x+=8)
for(int z=original.peakZ()-64;z<=original.peakZ()+64;z+=8){
int y=original.surface(x,z);if(y>=(coherent?262:278))candidates.add(new int[]{x,y,z});
}
candidates.sort(Comparator.<int[]>comparingInt(a->a[1]).reversed().thenComparingInt(a->a[0]).thenComparingInt(a->a[2]));
var chosen=new ArrayList<Crown>();
for(var p:candidates){
if(chosen.stream().anyMatch(c->Math.hypot(c.x-p[0],c.z-p[2])<34))continue;
double dx=1,dz=0;int low=1000;
for(int i=0;i<16;i++){
double angle=i*Math.PI/8;int y=original.surface(p[0]+(int)(28*Math.cos(angle)),p[2]+(int)(28*Math.sin(angle)));
if(y<low){low=y;dx=Math.cos(angle);dz=Math.sin(angle);}
}
chosen.add(new Crown(p[0],p[2],oldTop(p[0],p[2])-5.5,dx,dz));if(chosen.size()==3)break;
}
crowns=List.copyOf(chosen);
}
boolean coherent(){return coherent;}
List<Crown> crowns(){return crowns;}
private static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
private boolean inMassif(int x,int z){return Math.hypot(x-original.peakX(),z-original.peakZ())<132;}
private static double smooth(double t){t=Math.clamp(t,0,1);return t*t*(3-2*t);}
private double oldTop(int x,int z){
return oldTops.computeIfAbsent(key(x,z),k->{
for(int y=316;y>=240;y--){
float d=original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z);
if(d>0){float above=original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y+1,z);return y+d/(double)(d-above);}
}
return 239.0;
});
}
double roof(int x,int z){
if(!inMassif(x,z))return Double.POSITIVE_INFINITY;
return roofs.computeIfAbsent(key(x,z),k->{
double old=oldTop(x,z);if(old<260)return Double.POSITIVE_INFINITY;
double cut=0,first=Double.POSITIVE_INFINITY,second=Double.POSITIVE_INFINITY;
for(var c:crowns){
double ox=x-c.x,oz=z-c.z,distance=Math.hypot(ox,oz);
if(distance<first){second=first;first=distance;}else if(distance<second)second=distance;
double u=ox*c.dx+oz*c.dz,v=-ox*c.dz+oz*c.dx;
// Rounded footprint, independent of chunks. Leave gentler back slopes and connecting saddles.
double width=23+2*EcologyBiomes176.noise(192,x/13.0,z/13.0);
double weight=smooth((width-Math.abs(v))/8)*smooth((u+22)/12)*smooth((42-u)/14);
if(weight<=0)continue;
double lip=9+1.5*EcologyBiomes176.noise(c.x*7919L+c.z,x/9.0,z/9.0);
double plane=c.height+.035*u+.6*EcologyBiomes176.noise(1921,x/21.0,z/21.0)-4.6*Math.max(0,u-lip);
cut=Math.max(cut,weight*Math.clamp(old-plane,0,24));
}
// A narrow, softened Voronoi seam separates neighbouring crowns; never repeats across the island.
double seam=5*smooth((4-(second-first))/4)*smooth((31-first)/10);
cut=Math.max(cut,seam)*smooth((old-260)/15);
if(coherent){
// Shallow irregular gullies interrupt long contours, leaving the crown centres intact.
double edge=smooth((first-12)/9)*smooth((124-Math.hypot(x-original.peakX(),z-original.peakZ()))/16);
double grain=EcologyBiomes176.noise(193L^original.peakX()*7919L^original.peakZ(),x/11.0,z/13.0);
cut+=3.2*edge*smooth((grain+.12)/.7)*smooth((old-254)/18);
}
return cut>.01?Math.max(241,old-cut):Double.POSITIVE_INFINITY;
});
}
int surface(int x,int z){
// Neighbour samples need a real height beyond the mask, not an artificial cliff at its edge.
if(!inMassif(x,z))return Math.hypot(x-original.peakX(),z-original.peakZ())<140?(int)Math.floor(oldTop(x,z)):239;
return surfaces.computeIfAbsent(key(x,z),k->{
int start=(int)Math.min(316,Math.min(oldTop(x,z),Math.ceil(roof(x,z))-1));
for(int y=start;y>=240;y--)if(original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0 && y<roof(x,z))return y;
return 239;
});
}
float slope(int x,int z){
if(!inMassif(x,z)||surface(x,z)<248)return 0;
return slopes.computeIfAbsent(key(x,z),k->{
int center=surface(x,z);double slope=0;
// World-space columns cross chunk boundaries and ignore the separate aerial islands.
for(int[] d:new int[][]{{2,0},{-2,0},{0,2},{0,-2},{4,4},{-4,4},{4,-4},{-4,-4}})
slope=Math.max(slope,(center-surface(x+d[0],z+d[1]))/Math.hypot(d[0],d[1]));
double mask=smooth((132-Math.hypot(x-original.peakX(),z-original.peakZ()))/12);
return (float)(Math.min(8,slope)*mask);
});
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var f=s.field();var e=f.erosion();int changed=0,deep=0,steep=0,flat=0,maxDrop=0;var rows=new ArrayList<List<Integer>>();
for(int x=-320;x<=320;x+=8)for(int z=-320;z<=320;z+=8){
for(int y:new int[]{72,144,208,240,352,448}){
if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)!=e.original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z))throw new IllegalStateException("Erosion changed deep/sky density");deep++;
}
if(!e.inMassif(x,z))for(int y=248;y<320;y+=8)
if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)!=e.original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z))throw new IllegalStateException("Erosion escaped massif");
}
int rock=0,soil=0;
for(int x=f.peakX()-96;x<=f.peakX()+96;x+=2)for(int z=f.peakZ()-96;z<=f.peakZ()+96;z+=2){
int old=(int)Math.floor(e.oldTop(x,z)),now=e.surface(x,z);if(old<260)continue;
if(old!=now){changed++;maxDrop=Math.max(maxDrop,old-now);}
float slope=e.slope(x,z);if(slope>.95)steep++;else if(now>=(e.coherent?250:274) && slope<=.5)flat++;
if(slope>.95 && now>=260){
var b=level.getBlockState(new net.minecraft.core.BlockPos(x,now,z));
if(b.is(net.minecraft.world.level.block.Blocks.DIRT)||b.is(net.minecraft.world.level.block.Blocks.GRASS_BLOCK))soil++;
else if(AdventureCaves180.natural(b))rock++;
}
rows.add(List.of(x,z,old,now));
}
if(changed<40||flat<20||steep<40||(e.crowns.size()< (e.coherent?2:3)||e.crowns.size()>3)||rock<20||soil>rock)throw new IllegalStateException("Missing crown/face erosion: "+changed+" flat="+flat+" steep="+steep+" rock="+rock+" soil="+soil);
return Map.of("crowns",e.crowns,"changedColumnsAt2BlockSpacing",changed,"flatSummitSamples",flat,"steepSamples",steep,"steepRockSamples",rock,"steepSoilSamples",soil,"maxSurfaceDrop",maxDrop,"unchangedDeepAndSkySamples",deep,"heightSamples",rows);
}
}
@@ -0,0 +1,81 @@
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.*;
/** Opt-in disposable native-world test: offers, FULL completion, a chained relay and reload. */
final class Expansion199Checks {
private static Run run;
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,
CompletableFuture<ExpansionIsland> request,ItemStack payment,int stage) {}
static void register(){ServerTickEvents.END_SERVER_TICK.register(Expansion199Checks::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());
if(adapter==null)throw new IllegalStateException("expansion199 profile required");
var session=ExpansionRuntime.session(level);
result.put("verificationScope","expansion199");
if(fresh){
var sites=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites();
var pos=sites.get(0).anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
var core=level.getBlockState(AnchorOffers189.CORE);
if(!core.is(OriginBlock.BLOCK)||!level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK))throw new AssertionError("Missing native root/core");
level.setBlock(AnchorOffers189.CORE,core.setValue(OriginBlock.LIT,true),Block.UPDATE_ALL);
var wrong=new ItemStack(Items.DIRT,64);
if(!RelayOffers199.offer(level,pos,wrong,()->true).isCompletedExceptionally()||wrong.getCount()!=64)throw new AssertionError("Wrong offering accepted");
var demand=AnchorOffers189.DEMANDS.get(0);var held=new ItemStack(demand.item(),demand.count()+1);
var request=RelayOffers199.offer(level,pos,held,()->true);
if(!RelayOffers199.offer(level,pos,held,()->true).isCompletedExceptionally())throw new AssertionError("Duplicate search accepted");
run=new Run(server,output,result,started,true,request,held,0);
}else{
if(session.islands().size()!=3)throw new AssertionError("Journal did not retain both expansions");
run=new Run(server,output,result,started,false,CompletableFuture.completedFuture(session.islands().get(2)),ItemStack.EMPTY,2);
}
}
private static void tick(MinecraftServer server){
var r=run;if(r==null||r.server()!=server)return;
try{
if(System.nanoTime()-r.started()>java.util.concurrent.TimeUnit.MINUTES.toNanos(8))throw new AssertionError("Expansion test timed out");
if(!r.request().isDone())return;
var admitted=r.request().join();var level=server.overworld();var session=ExpansionRuntime.session(level);
var current=session.island(admitted.id());
if(!current.status().equals("ready")){
if(r.stage()==1){
// Persist a partially prepared reservation; the warm run must complete it.
r.result().put("reservedChild",current.id());finish(r);return;
}
if(session.progress().contains(": ")&&!session.progress().matches(".*: [0-9]+/[0-9]+"))throw new AssertionError(session.progress());
return;
}
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());var relay=adapter.relay(current);
if(!level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK))throw new AssertionError("Ready before next relay exists");
if(r.stage()==0){
if(r.payment().getCount()!=1)throw new AssertionError("Offer charged incorrectly");
var rootSite=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().get(0).anchor();
if(RelayOffers199.offer(level,rootSite,r.payment(),()->true).join().id().equals(current.id())&&r.payment().getCount()!=1)throw new AssertionError("Repeated offer charged");
r.result().put("firstExpansion",Map.of("id",current.id(),"x",current.centerX(),"z",current.centerZ(),"relayY",relay.getY()));
int sector=level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR);var demand=AnchorOffers189.DEMANDS.get(sector);
var held=new ItemStack(demand.item(),demand.count()+1);
var request=RelayOffers199.offer(level,relay,held,()->true);
run=new Run(server,r.output(),r.result(),r.started(),true,request,held,1);
}else{
int columns=0,solid=0;int low=1600,high=0;
for(int x=-64;x<=64;x+=16)for(int z=-64;z<=64;z+=16){
var chunk=level.getChunk((current.centerX()+x)>>4,(current.centerZ()+z)>>4);columns++;
for(int y=0;y<384;y++)if(!chunk.getBlockState(new BlockPos(current.centerX()+x,y,current.centerZ()+z)).isAir()){solid++;low=Math.min(low,y);high=Math.max(high,y);}
}
if(solid<columns*4||solid>columns*110||low<140||high>325)throw new AssertionError("Unexpected sheet terrain "+solid+" / "+columns+" y="+low+".."+high);
r.result().put("resumedExpansion",Map.of("id",current.id(),"columns",columns,"solid",solid,"minY",low,"maxY",high));
finish(r);
}
}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());}
}
@@ -0,0 +1,59 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.chunk.*;
/** The existing rock rooms stay intact; masonry and light give the horizontal portal a focal point. */
final class Gallery189 {
private static final Map<AdventureBiomes180,Map<BlockPos,BlockState>> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
private static void put(Map<BlockPos,BlockState> m,int x,int y,int z,Block b){m.put(new BlockPos(x,y,z),b.defaultBlockState());}
static Map<BlockPos,BlockState> plan(AdventureBiomes180 s){synchronized(PLANS){return PLANS.computeIfAbsent(s,Gallery189::create);}}
private static Map<BlockPos,BlockState> create(AdventureBiomes180 s){
var m=new LinkedHashMap<BlockPos,BlockState>();var gallery=Cavern183.plan(s);int cy=gallery.rooms().getFirst().y();
for(var c:gallery.cells().values()){
int ring=Math.max(Math.abs(c.x()),Math.abs(c.z()));
if(ring<=2)continue; // Keep every block of the existing portal rim and recess.
if(ring>=3&&ring<=8){
Block b=ring==3||ring==8?Blocks.POLISHED_DEEPSLATE:ring==5?Blocks.CHISELED_TUFF:Blocks.STONE_BRICKS;
put(m,c.x(),c.y(),c.z(),b);
}else if(c.path())put(m,c.x(),c.y(),c.z(),Blocks.POLISHED_ANDESITE);
}
// Embedded light rhythm follows all three passages; no wall cladding or added excavation.
int index=0;
for(var p:gallery.route())if(index++%10==0&&Math.hypot(p.getX(),p.getZ())>9){
var c=gallery.cells().get(key(p.getX(),p.getZ()));if(c==null)continue;
put(m,c.x(),c.y()-1,c.z(),Blocks.GLOWSTONE);
put(m,c.x(),c.y(),c.z(),Blocks.COPPER_GRATE.waxed().oxidized());
}
// A restrained suspended stone cross, fixed to the actual gallery roof, five blocks above the rim.
var center=gallery.cells().get(key(0,0));int height=cy+7;
for(int y=height+1;y<=center.ceiling()+1;y++)put(m,0,y,0,Blocks.COPPER_CHAIN.waxed().oxidized());
put(m,0,height,0,Blocks.CHISELED_TUFF);
for(var d:Direction.Plane.HORIZONTAL){
put(m,d.getStepX(),height,d.getStepZ(),Blocks.POLISHED_TUFF_SLAB);
var lamp=new BlockPos(d.getStepX()*2,height-1,d.getStepZ()*2);
put(m,lamp.getX(),height,lamp.getZ(),Blocks.POLISHED_TUFF_SLAB);
m.put(lamp,Blocks.COPPER_LANTERN.waxed().oxidized().defaultBlockState().setValue(BlockStateProperties.HANGING,true));
}
for(int[] p:new int[][]{{-7,-7},{7,-7},{-7,7},{7,7}}){
var c=gallery.cells().get(key(p[0],p[1]));if(c==null)continue;
put(m,p[0],c.y()-1,p[1],Blocks.GLOWSTONE);put(m,p[0],c.y(),p[1],Blocks.COPPER_GRATE.waxed().oxidized());
}
return Map.copyOf(m);
}
static void place(AdventureBiomes180 s,ChunkAccess c){plan(s).forEach((p,b)->{if(c.getPos().contains(p))c.setBlockState(p,b);});}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
int light=0;for(var e:plan(s).entrySet()){
if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("Gallery ornament overwritten "+e.getKey());
if(e.getValue().getLightEmission()>0)light++;
}
if(light<8)throw new IllegalStateException("Insufficient portal lighting");
return Map.of("ornamentBlocks",plan(s).size(),"lightSources",light,"newCarvedBlocks",0,"portal",Cavern183.plan(s).rooms().getFirst());
}
}
@@ -0,0 +1,38 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.RandomState;
/** Opt-in laboratory verification; never runs during ordinary world creation. */
final class Gallery198Checks {
static List<Map<String,Object>> check(ServerLevel level) {
var rows=new ArrayList<Map<String,Object>>();
var biomes=((AdventureBiomes180)level.getChunkSource().getGenerator().getBiomeSource()).collectPossibleBiomes().toList();
for(var size:SharedIsland196.Size.values())for(long seed:new long[]{0,17,42,4736390610738281858L,Long.MIN_VALUE}) {
var settings=level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS,size.id())).value();
var random=RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE),seed,settings);
var field=(ReliefEcology177.Field)random.getSampler(settings.noiseRouter().finalDensity());
var current=new AdventureBiomes180(biomes,true,true,true,true,true);current.bind(seed,field);
var plan=Cavern183.plan(current);
var centre=plan.rooms().getFirst();
if(centre.x()!=0||centre.z()!=0||centre.y()<32||centre.y()>276)
throw new AssertionError("Invalid mandatory gallery: "+size+" "+seed+" "+centre);
var repeat=new AdventureBiomes180(biomes,true,true,true,true,true);repeat.bind(seed,field);
if(!plan.equals(Cavern183.plan(repeat)))throw new AssertionError("Non-deterministic gallery");
var historical=new AdventureBiomes180(biomes,true,true,true,true);historical.bind(seed,field);
boolean fallback=false;
try {
if(!plan.equals(Cavern183.plan(historical)))throw new AssertionError("Previously valid gallery changed");
} catch(IllegalStateException e) {
if(!"No supported central gallery floor".equals(e.getMessage()))throw e;
fallback=true;
}
rows.add(Map.of("size",size.name(),"seed",seed,"floorY",centre.y(),"newFallback",fallback));
}
return rows;
}
}
@@ -0,0 +1,25 @@
package fr.koka.sanctuarytest;
/** Last-resort admission for the mandatory portal, bounded to its existing footprint. */
final class GalleryFloor198 {
@FunctionalInterface interface Rock { boolean at(int x,int y,int z); }
private GalleryFloor198() {}
static int choose(Rock rock) {
int best=164,bestScore=Integer.MIN_VALUE;
// A central shaft is not evidence that the whole chamber has no supporting rock.
// Survey the floor and canopy, including thin shelves above the old Y=240 limit.
for(int y=32;y<=276;y++) {
int score=-Math.abs(y-164);
for(int x=-16;x<=16;x+=8)for(int z=-12;z<=12;z+=6) {
if(x*x/361.0+z*z/256.0>1)continue;
if(rock.at(x,y-3,z))score+=12;
if(rock.at(x,y+20,z))score+=6;
}
if(score>bestScore){bestScore=score;best=y;}
}
// Even a fully open shaft retains the existing three-block gallery foundation.
// No seed may abort world creation merely because this required room has no cave.
return best;
}
}
@@ -0,0 +1,76 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.gems.Gems;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
/** Three finite districts, at most twelve blocks per owned chunk, prioritising natural cave faces. */
public final class Gems183 {
record District(String name,int x,int z,Block ore,Block deep) {}
static final List<District> DISTRICTS=List.of(
new District("sapphire",0,-176,Gems.SAPPHIRE.ore(),Gems.SAPPHIRE.deepslateOre()),
new District("emerald",176,0,Blocks.EMERALD_ORE,Blocks.DEEPSLATE_EMERALD_ORE),
new District("ruby",0,176,Gems.RUBY.ore(),Gems.RUBY.deepslateOre()));
private Gems183() {}
private static List<District> districts(AdventureBiomes180 source) {
double spread=source.field().base().spread();
if(spread==1)return DISTRICTS;
return DISTRICTS.stream().map(d->new District(d.name,(int)Math.round(d.x*spread),(int)Math.round(d.z*spread),d.ore,d.deep)).toList();
}
private static boolean host(BlockState b){return b.is(BlockTags.BASE_STONE_OVERWORLD)||b.is(Blocks.BASALT)||b.is(Blocks.BLACKSTONE);}
private static boolean deep(BlockState b){return b.is(Blocks.DEEPSLATE)||b.is(Blocks.TUFF)||b.is(Blocks.BASALT)||b.is(Blocks.BLACKSTONE);}
private static boolean inside(District d,ChunkPos p){return Math.hypot(p.getMiddleBlockX()-d.x,p.getMiddleBlockZ()-d.z)<=64;}
private static long hash(long seed,BlockPos p){long n=seed^p.asLong();n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
if(!WorldgenLab.discovery183(generator)&&!WorldgenLab.ascent184(generator)&&!WorldgenLab.stem185(generator)&&!WorldgenLab.anchors186(generator)&&!WorldgenLab.natural187(generator)&&!WorldgenLab.details188(generator)&&!WorldgenLab.finishedTerrain(generator))return;
var s=(AdventureBiomes180)((NoiseBasedChunkGenerator)generator).getBiomeSource();
for(var district:districts(s)){
if(!inside(district,chunk.getPos()))continue;
var candidates=new ArrayList<BlockPos>();
int minX=chunk.getPos().getMinBlockX(),minZ=chunk.getPos().getMinBlockZ();
for(int x=minX+2;x<minX+14;x+=2)for(int z=minZ+2;z<minZ+14;z+=2)for(int y=40;y<=Math.min(286,s.terrain(x,z).top()-14);y++){
var p=new BlockPos(x,y,z);if(!host(chunk.getBlockState(p))||Dungeon180.protectedAt(s,x,y,z))continue;
boolean exposed=false;for(var side:Direction.values())if(chunk.getBlockState(p.relative(side)).isAir()){exposed=true;break;}
if(exposed)candidates.add(p);
}
candidates.sort(Comparator.comparingLong(p->hash(level.getSeed()^0x1836EAL,p)));
int placed=0;
for(var p:candidates){
if(placed>=12)break;
// Short clusters around an exposed nucleus, no pockets suspended in the air.
for(var at:List.of(p,p.above(),p.east(),p.south())){
if(placed>=12)break;
var b=chunk.getBlockState(at);
if(!host(b)||Dungeon180.protectedAt(s,at.getX(),at.getY(),at.getZ()))continue;
chunk.setBlockState(at,(deep(b)?district.deep:district.ore).defaultBlockState());placed++;
}
}
}
}
static Map<String,Object> check(ServerLevel level){
var result=new LinkedHashMap<String,Object>();
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();
for(var d:districts(source)){
int count=0,exposed=0,chunks=0;BlockPos first=null;
for(int cx=(d.x-72)>>4;cx<=(d.x+72)>>4;cx++)for(int cz=(d.z-72)>>4;cz<=(d.z+72)>>4;cz++){
var cp=new ChunkPos(cx,cz);if(!inside(d,cp))continue;chunks++;var chunk=level.getChunk(cx,cz);
for(int x=cp.getMinBlockX();x<=cp.getMaxBlockX();x++)for(int z=cp.getMinBlockZ();z<=cp.getMaxBlockZ();z++)for(int y=40;y<=288;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);if(!b.is(d.ore)&&!b.is(d.deep))continue;
count++;boolean air=false;for(var side:Direction.values())if(level.getBlockState(p.relative(side)).isAir()){air=true;break;}
if(air){exposed++;if(first==null)first=p;}
}
}
if(count<200||count>chunks*12||exposed<40)throw new IllegalStateException("Gem district stock/exposure "+d.name+": "+count+" / "+exposed);
result.put(d.name,Map.of("blocks",count,"exposed",exposed,"chunkCeiling",chunks*12,"visit",first.toShortString()));
}
return result;
}
}
@@ -0,0 +1,30 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.resources.*;
import net.minecraft.core.registries.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.level.storage.loot.*;
import net.minecraft.world.level.storage.loot.parameters.*;
import net.minecraft.world.phys.Vec3;
final class IronLoot188 {
static Map<String,Object> check(ServerLevel level){
return check(level,"chests/mine_cache_188");
}
static Map<String,Object> check(ServerLevel level,String path){
var table=level.getServer().reloadableRegistries().getLootTable(ResourceKey.create(Registries.LOOT_TABLE,Identifier.fromNamespaceAndPath("sanctuary_test",path)));
int enchanted=0;var kinds=new HashSet<String>();
for(int i=0;i<32;i++){
var items=table.getRandomItems(new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN,new Vec3(i,150,0)).create(LootContextParamSets.CHEST));
int pieces=0;for(var item:items){
String id=BuiltInRegistries.ITEM.getKey(item.getItem()).toString();
if(id.startsWith("minecraft:iron_")&&!id.equals("minecraft:iron_ingot")){
if(item.getOrDefault(DataComponents.ENCHANTMENTS,net.minecraft.world.item.enchantment.ItemEnchantments.EMPTY).isEmpty())throw new IllegalStateException("Unenchanted mine equipment "+item);
pieces++;kinds.add(id);
}
}
if(pieces<1||pieces>2)throw new IllegalStateException("Expected 1–2 iron pieces, got "+pieces);enchanted+=pieces;
}
return Map.of("lootRolls",32,"enchantedPieces",enchanted,"equipment",kinds);
}
}
@@ -0,0 +1,101 @@
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());
}
}
@@ -0,0 +1,49 @@
package fr.koka.sanctuarytest;
import com.google.gson.JsonParser;
import fr.koka.sanctuary.SanctuaryMod;
import java.nio.file.*;
import java.util.List;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.screens.BackupConfirmScreen;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.storage.LevelResource;
/** Explicit launcher option for a fresh disposable visit; no photos, camera control or ordinary-world prompts. */
final class LabVisit200 {
private static boolean confirmed,identity,ready;
static void register(){
var argument=System.getProperty("sanctuary.labVisit200");if(argument==null)return;
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");
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())){
for(var child:screen.children())if(child instanceof Button b&&b.getMessage().getString().equals(Component.translatable("selectWorld.backupJoinSkipButton").getString())){
confirmed=true;mc.execute(()->{if(mc.gui.screen()==screen)b.onPress(new KeyEvent(257,0,0));});break;
}
}
if(!identity&&screen instanceof fr.koka.sanctuary.client.HelloWorldScreen){
identity=true;mc.execute(()->{
if(mc.gui.screen()!=screen)return;var enter=new KeyEvent(257,0,0);
for(var suffix:List.of("$ColorButton","$StarterSlot"))for(var child:screen.children())if(child instanceof Button b&&child.getClass().getName().endsWith(suffix)&&b.active){b.onPress(enter);break;}
for(var child:screen.children())if(child instanceof EditBox box&&box.getMessage().getString().equals(Component.translatable("sanctuary.starter.name").getString()))box.setValue("Labo");
for(var child:screen.children())if(child instanceof Button b&&b.active&&b.getMessage().getString().equals(fr.koka.sanctuary.progression.ProgressionService.label("enter").getString())){b.onPress(enter);break;}
});
}
});
ClientTickEvents.END_CLIENT_TICK.register(mc->{
if(ready||mc.player==null||mc.level==null||mc.gui.screen()!=null||mc.getSingleplayerServer()==null)return;
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());
});
}catch(Exception e){throw new IllegalStateException("Invalid North visit manifest",e);}
}
}
@@ -0,0 +1,69 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Retain the accepted basins; settle the original bed before any trees are placed. */
public final class Lakes188 {
record Bed(BlockPos pos,BlockState material,int surface) {}
private static final Map<AdventureBiomes180,List<Bed>> BEDS=Collections.synchronizedMap(new WeakHashMap<>());
private static boolean rock(AdventureBiomes180 s,BlockPos p){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())>0;}
static List<Bed> beds(AdventureBiomes180 s){synchronized(BEDS){return BEDS.computeIfAbsent(s,key->{
var result=new ArrayList<Bed>();
for(var pool:SurfaceWater187.plan(s).pools())for(var water:pool.cells()){
var p=water.below();if(pool.cells().contains(p)||!rock(s,p))continue;
double n=EcologyBiomes176.noise(s.seed()^0x188BEDL,p.getX()/9.0,p.getZ()/9.0);
Block b=n<-.3?Blocks.CLAY:n<0?Blocks.MUD:n<.3?Blocks.SAND:Blocks.GRAVEL;
if((b==Blocks.SAND||b==Blocks.GRAVEL)&&!rock(s,p.below()))b=Blocks.CLAY;
result.add(new Bed(p,b.defaultBlockState(),pool.surface()));
}
return List.copyOf(result);
});}}
public static void prepare(WorldGenLevel level,ChunkGenerator g,ChunkAccess c){
if(!WorldgenLab.details188(g))return;var s=Anchors188.source(g);
SurfaceWater187.place(s,c);
for(var b:beds(s))if(c.getPos().contains(b.pos))c.setBlockState(b.pos,b.material);
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos p){
if(!WorldgenLab.details188(g)||p.getY()<140||p.getY()>320)return false;
var s=Anchors188.source(g);int top=SurfaceWater187.surfaceAt(s,p.getX(),p.getZ());
// Reserve the submerged rooting column too, so a trunk cannot survive above rejected underwater writes.
return top>=0&&p.getY()>=top-29&&p.getY()<=top+20;
}
static void decorate(AdventureBiomes180 s,ChunkAccess c){
for(var b:beds(s))if(c.getPos().contains(b.pos)){
c.setBlockState(b.pos,b.material);var p=b.pos.above();
if(!c.getBlockState(p).is(Blocks.WATER))continue;
long grain=net.minecraft.util.Mth.getSeed(b.pos)^s.seed();
if(Math.floorMod(grain,7)!=0)continue;
int depth=b.surface-p.getY();
if(depth>=3&&Math.floorMod(grain,3)==0){
int height=Math.min(depth,2+(int)Math.floorMod(grain,3));
for(int h=0;h<height;h++){
var q=p.above(h);if(!c.getBlockState(q).is(Blocks.WATER))break;
c.setBlockState(q,(h==height-1?Blocks.KELP:Blocks.KELP_PLANT).defaultBlockState());
}
}else if(depth>=1)c.setBlockState(p,Blocks.SEAGRASS.defaultBlockState());
}
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var counts=new TreeMap<String,Integer>();int plants=0,logs=0;
for(var b:beds(s)){
var actual=level.getBlockState(b.pos);
if(!actual.equals(b.material))throw new IllegalStateException("Lake sediment overwritten "+b.pos+" "+actual);
counts.merge(actual.getBlock().toString(),1,Integer::sum);
var plant=level.getBlockState(b.pos.above());if(plant.is(Blocks.SEAGRASS)||plant.is(Blocks.KELP)||plant.is(Blocks.KELP_PLANT))plants++;
}
for(var pool:SurfaceWater187.plan(s).pools())for(var p:pool.cells())if(p.getY()==pool.surface())
for(int y=p.getY()-28;y<=p.getY()+20;y++)if(level.getBlockState(new BlockPos(p.getX(),y,p.getZ())).is(BlockTags.LOGS))logs++;
if(counts.size()<4||plants<50||logs!=0)throw new IllegalStateException("Lake ecology failure sediments="+counts+" plants="+plants+" logs="+logs);
return Map.of("sediments",counts,"plantedColumns",plants,"treesInWater",logs);
}
}
@@ -0,0 +1,25 @@
package fr.koka.sanctuarytest;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Cached local slope of the sculpted mountain, independent of chunk edges and sky heightmaps. */
public record MassifSlope192(DensityFunction terrain) implements DensityFunction {
public static final MapCodec<MassifSlope192> CODEC=RecordCodecBuilder.mapCodec(i->i.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(MassifSlope192::terrain)
).apply(i,MassifSlope192::new));
@Override public DensitySampler compileSampler(CompileContext context){
var field=(ReliefEcology177.Field)DfRewriteRule.INLINE_REFERENCE.rewrite(terrain).compileSampler(context);
if(!field.eroded())throw new IllegalArgumentException("Massif slope requires eroded terrain");
return new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return field.erosion().slope(x,z);}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
}
@Override public DensityFunction rewriteChildren(DfRewriteRule rule){return new MassifSlope192(rule.rewrite(terrain));}
@Override public Interval range(){return Interval.of(0,8);}
@Override public int domainAxes(){return AXIS_X|AXIS_Z;}
@Override public MapCodec<MassifSlope192> codec(){return CODEC;}
}
@@ -0,0 +1,96 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Fill existing connected voids up to their natural spill height; never carve or lay a basin bed. */
final class NaturalWater184 {
record Pool(BlockPos seed,int surface,Set<BlockPos> cells,int area) {}
record Plan(List<Pool> pools,int probes,long millis) {}
private record Candidate(BlockPos floor,int score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private NaturalWater184() {}
static Plan plan(AdventureBiomes180 s){synchronized(PLANS){return PLANS.computeIfAbsent(s,NaturalWater184::find);}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private static boolean reserved(AdventureBiomes180 s,int x,int z){
if(Math.hypot(x,z)<(s.field().coherent()?88:72)||Math.hypot(x+152,z-152)<62||Dungeon180.near(s,x,z,55))return true;
for(var b:Basins180.sites(s))if(Math.hypot(b.x()-x,b.z()-z)<90)return true;
return false;
}
private static Plan find(AdventureBiomes180 s){
long start=System.nanoTime();var candidates=new ArrayList<Candidate>();
double spread=s.field().base().spread();int step=(int)Math.round(16*spread),extent=18*step;
for(int x=-extent;x<=extent;x+=step)for(int z=-extent;z<=extent;z+=step){
if(Math.hypot(x,z)>310*spread||reserved(s,x,z))continue;
// Include naturally open alcoves under the plateau, not only the highest terrain column.
for(int y=268;y>=160;y--)if(rock(s,x,y,z)&&!rock(s,x,y+1,z)&&!rock(s,x,y+3,z)){
int score=0;
for(var d:Direction.Plane.HORIZONTAL){
if(rock(s,x+d.getStepX()*12,y+5,z+d.getStepZ()*12))score+=3;
if(rock(s,x+d.getStepX()*24,y+5,z+d.getStepZ()*24))score++;
}
if(score>=6)candidates.add(new Candidate(new BlockPos(x,y,z),score));
}
}
candidates.sort(Comparator.comparingInt(Candidate::score).reversed());
var pools=new ArrayList<Pool>();int probes=0;
for(var candidate:candidates){
if(probes>=160||pools.size()==2)break;
var at=candidate.floor();if(pools.stream().anyMatch(p->p.seed().distSqr(at)<100*100))continue;
Pool best=null;
for(int rise=2;rise<=12;rise+=2){
probes++;var pool=flood(s,at.above(),at.getY()+rise);
if(pool==null)break;
if(pool.area()>=180&&pool.cells().size()>=450)best=pool;
}
if(best!=null){var retained=best;
if(!s.field().coherent()||pools.stream().noneMatch(p->Continuity193.touches(p.cells(),retained.cells())))pools.add(retained);
}
}
return new Plan(List.copyOf(pools),probes,(System.nanoTime()-start)/1_000_000);
}
private static Pool flood(AdventureBiomes180 s,BlockPos seed,int surface){
var queue=new ArrayDeque<BlockPos>();var seen=new HashSet<BlockPos>();var wet=new HashSet<BlockPos>();queue.add(seed);int area=0;
while(!queue.isEmpty()){
var p=queue.removeFirst();if(p.getY()>surface||!seen.add(p)||rock(s,p.getX(),p.getY(),p.getZ()))continue;
// A void leaving this finite box is an escape, never an artificial retaining wall.
if(Math.abs(p.getX()-seed.getX())>=52||Math.abs(p.getZ()-seed.getZ())>=52||p.getY()<surface-24||wet.size()>=12000)return null;
if(reserved(s,p.getX(),p.getZ()))return null;
wet.add(p);if(p.getY()==surface)area++;
for(var d:Direction.values())queue.add(p.relative(d));
}
return new Pool(seed,surface,Set.copyOf(wet),area);
}
static void place(AdventureBiomes180 s,ChunkAccess chunk){
for(var pool:plan(s).pools())for(var at:pool.cells())if(chunk.getPos().contains(at)&&chunk.getBlockState(at).isAir()){
chunk.setBlockState(at,Blocks.WATER.defaultBlockState());chunk.markPosForPostProcessing(at);
}
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var plan=plan(s);var details=new ArrayList<Map<String,Object>>();
for(var pool:plan.pools()){
var chunks=new HashSet<net.minecraft.world.level.ChunkPos>();for(var p:pool.cells())chunks.add(new net.minecraft.world.level.ChunkPos(p.getX()>>4,p.getZ()>>4));
for(var cp:chunks)level.getChunk(cp.x(),cp.z());
int water=0,leaks=0;
for(var p:pool.cells()){
if(rock(s,p.getX(),p.getY(),p.getZ()))throw new IllegalStateException("Water carved natural rock "+p);
if(!level.getBlockState(p).is(Blocks.WATER))continue;water++;
for(var d:Direction.values())if(d!=Direction.UP){
var side=p.relative(d);var b=level.getBlockState(side);
if(!pool.cells().contains(side)&&b.getCollisionShape(level,side).isEmpty()&&b.getFluidState().isEmpty())leaks++;
}
}
if(water<400||leaks!=0)throw new IllegalStateException("Natural water volume/leak failure "+water+" / "+leaks+" at "+pool.seed());
details.add(Map.of("seed",pool.seed().toShortString(),"surfaceY",pool.surface(),"surfaceArea",pool.area(),"waterBlocks",water,"leaks",leaks));
}
// The historical Medium witness requires this extra lake. A sized island may have no
// enclosed alcove; its mandatory cave basins and surface water are checked separately.
if(details.isEmpty()&&SharedIsland196.size(level.getChunkSource().getGenerator())==null)
throw new IllegalStateException("No retained natural lake; candidates tried="+plan.probes());
return Map.of("pools",details,"planningMs",plan.millis(),"boundedFloodAttempts",plan.probes(),"carvedRock",0,"artificialBed",0);
}
}
@@ -0,0 +1,105 @@
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.*;
/** Opt-in fresh-world integration: actual offering, interrupted preparation, village and frozen lake. */
final class North200Checks {
private record Run(MinecraftServer server,Path output,Map<String,Object> result,long started,boolean fresh,int revision,CompletableFuture<ExpansionIsland> request,ItemStack payment) {}
private static Run run;
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;
require(adapter!=null&&adapter.rootGeneration()==revision,"Wrong profile");
var session=ExpansionRuntime.session(level);result.put("verificationScope","north"+revision);
if(fresh){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.sector()==0).findFirst().orElseThrow();
var pos=site.anchor();level.getChunkAt(pos);level.getChunkAt(AnchorOffers189.CORE);
require(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK),"Missing north 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 payment=new ItemStack(Items.STONE,33);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,revision,request,payment);
}else{
require(session.islands().size()==2,"Reservation not restored");
run=new Run(server,output,result,started,false,revision,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(admitted.generation()==r.revision()&&admitted.diameter()==(r.revision()>=201?1024:2048)&&admitted.direction().equals("north")&&admitted.climate().equals("glacial"),"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 interruption during 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 village=adapter.village(island);
require(adapter.preparationChunks(island).size()<=64,"Unbounded preparation");
var relay=village.relay();level.getChunkAt(relay);require(level.getBlockState(relay).is(ExpansionAnchorBlock.BLOCK),"No continuation relay");
require(level.getBlockState(relay).getValue(ExpansionAnchorBlock.SECTOR)==0,"Relay identity changed");
int beds=0,cellars=0;
for(var house:village.houses()){
var centre=house.at(3,0,3);level.getChunkAt(centre);
for(int x=0;x<7;x++)for(int z=0;z<8;z++)if(level.getBlockState(house.at(x,1,z)).getBlock() instanceof BedBlock)beds++;
var trap=house.at(3,0,5);
if(house.cellar()){
require(level.getBlockState(trap).getBlock() instanceof TrapDoorBlock,"No secret hatch");
for(int y=-1;y>=-16;y--)require(level.getBlockState(house.at(3,y,5)).is(Blocks.LADDER),"Broken cellar ladder at "+house.at(3,y,5));
require(level.getBlockState(house.at(1,-17,4)).is(Blocks.CHEST),"No cellar chest");
require(level.getBlockState(house.at(5,-16,4)).is(Blocks.BREWING_STAND),"No curing station");cellars++;
}else require(level.getBlockState(trap).is(Blocks.SNOW_BLOCK),"Extra secret hatch");
}
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));
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;
for(int x=lake.x()-reach;x<=lake.x()+reach;x++)for(int z=lake.z()-reach;z<=lake.z()+reach;z++){
var c=f.column(x,z);if(c.water()<0)continue;
var lid=new BlockPos(x,c.water(),z);require(level.getBlockState(lid).is(Blocks.ICE),"Missing frozen lid "+lid);ice++;
for(int y=(int)Math.floor(c.top())+1;y<c.water();y++){
var p=new BlockPos(x,y,z);require(!level.getBlockState(p).getFluidState().isEmpty(),"Dry lake "+p);water++;
for(var n:List.of(p.north(),p.south(),p.east(),p.west(),p.below()))require(!level.getBlockState(n).isAir(),"Lake leak into air "+n);
}
}
require(ice>800&&water>2000,"Missing lake");r.result().put("frozenLake",Map.of("x",lake.x(),"y",lake.level(),"z",lake.z(),"ice",ice,"water",water));
// A few actual columns in distant peaks: newly explored terrain after ready, not a blanket pre-generation.
int frozen=0,cavities=0;double highest=0;BlockPos peak=null;
for(int x=-750;x<=750;x+=50)for(int z=-750;z<=750;z+=50){var c=f.column(island.centerX()+x,island.centerZ()+z);double score=c.top();if(r.revision()==203){var cover=NorthTerrain203.field(island).cover(island.centerX()+x,island.centerZ()+z);if(cover.caveHigh()-cover.caveLow()<12)continue;score=cover.caveHigh()-cover.caveLow()+c.top()*.01;}if(c.edge()>100&&score>highest){highest=score;peak=new BlockPos(island.centerX()+x,(int)c.top(),island.centerZ()+z);}}
for(int dx=-16;dx<=16;dx+=4)for(int dz=-16;dz<=16;dz+=4){
int x=peak.getX()+dx,z=peak.getZ()+dz;var chunk=level.getChunk(x>>4,z>>4);var c=f.column(x,z);
for(int y=(int)c.bottom()+12;y<c.top()-12;y++){
var state=chunk.getBlockState(new BlockPos(x,y,z));if(state.is(Blocks.BLUE_ICE)||state.is(Blocks.PACKED_ICE))frozen++;if(state.isAir())cavities++;
}
}
require(frozen>20&&cavities>30,"No native ice cavern: "+frozen+"/"+cavities);
r.result().put("iceCavern",Map.of("peak",peak.toShortString(),"ice",frozen,"air",cavities));
if(r.revision()==203)North203Checks.verify(level,island,village,r.result());
if(r.revision()==202)North202Checks.verify(level,island,village,r.result());
if(r.revision()==201)North201Checks.verify(level,island,village,r.result());
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 require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,44 @@
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;
/** Verify actual native decorations and visible surfaces, beyond biome IDs or analytic density. */
final class North201Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain201.field(island);var wood=f.woodland();int logs=0,leaves=0;var trunks=new HashSet<Long>();
for(int dx=-24;dx<=24;dx++)for(int dz=-24;dz<=24;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);
for(int y=(int)c.top()-2;y<=c.top()+24;y++){
var p=new BlockPos(x,y,z);var b=chunk.getBlockState(p);
if(b.is(Blocks.SPRUCE_LOG)){logs++;trunks.add(((long)x<<32)^(z&0xffffffffL));}
if(b.is(Blocks.SPRUCE_LEAVES))leaves++;
}
}
require(trunks.size()>=10&&logs>=50&&leaves>=100,"Native woodland absent: "+trunks.size()+" trunks, "+logs+" logs, "+leaves+" leaves");
result.put("nativeWoodland",Map.of("centre",wood.toShortString(),"trunks",trunks.size(),"logs",logs,"leaves",leaves));
int iceAbove=0,tallest=0,spikedColumns=0;
int cx=island.centerX()-145,cz=island.centerZ()-115;
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=cx+dx,z=cz+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);int height=0;
for(int y=(int)Math.ceil(c.top())+2;y<c.top()+64;y++)if(chunk.getBlockState(new BlockPos(x,y,z)).is(Blocks.PACKED_ICE)){iceAbove++;height=Math.max(height,y-(int)Math.floor(c.top()));}
if(height>=5)spikedColumns++;tallest=Math.max(tallest,height);
}
require(iceAbove>=100&&tallest>=7&&spikedColumns>=15,"Native ice spikes absent: "+iceAbove+" ice, max height "+tallest);
result.put("nativeIceSpikes",Map.of("x",cx,"z",cz,"iceAboveTerrain",iceAbove,"tallest",tallest,"columns",spikedColumns));
int visibleIce=0,snowCovered=0;var lake=f.lakes().getFirst();
for(int dx=-45;dx<=45;dx++)for(int dz=-45;dz<=45;dz++){
int x=lake.x()+dx,z=lake.z()+dz;var c=f.column(x,z);if(c.water()<0)continue;
var p=new BlockPos(x,c.water(),z);
if(level.getBlockState(p).is(Blocks.ICE)&&level.getBlockState(p.above()).isAir())visibleIce++;
if(level.getBlockState(p.above()).is(Blocks.SNOW)||level.getBlockState(p.above()).is(Blocks.SNOW_BLOCK))snowCovered++;
}
require(visibleIce>2500&&snowCovered<visibleIce/20,"Lake hidden by snow: "+visibleIce+" visible / "+snowCovered+" hidden");
require(Math.hypot(village.arrival().getX()-lake.x(),village.arrival().getZ()-lake.z())<220,"Village too far from lake");
result.put("visibleLake",Map.of("ice",visibleIce,"snowCovered",snowCovered));
}
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;
/** Inspect actual native vegetation, snow and exposed rock in a fresh 202 world. */
final class North202Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain202.field(island);var wood=f.sample(NorthTerrain202.Surface.OLD_GROWTH);
int logs=0,leaves=0,podzol=0,tallestTree=0;var trunks=new HashSet<Long>();
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);int trunk=0;
for(int y=(int)c.top()-2;y<=c.top()+40;y++){
var state=chunk.getBlockState(new BlockPos(x,y,z));
if(state.is(Blocks.SPRUCE_LOG)){logs++;trunk++;trunks.add(((long)x<<32)^(z&0xffffffffL));}
if(state.is(Blocks.SPRUCE_LEAVES))leaves++;
if(state.is(Blocks.PODZOL))podzol++;
}
tallestTree=Math.max(tallestTree,trunk);
}
int wide=0;for(long key:trunks){int x=(int)(key>>32),z=(int)key;if(trunks.contains(((long)(x+1)<<32)^(z&0xffffffffL))&&trunks.contains(((long)x<<32)^((z+1)&0xffffffffL))&&trunks.contains(((long)(x+1)<<32)^((z+1)&0xffffffffL)))wide++;}
require(wide>0&&tallestTree>=18&&logs>100&&leaves>100&&podzol>100,"Native giant spruce absent: "+wide+" wide, "+tallestTree+" tall, "+logs+" logs");
result.put("nativeGiantTaiga",Map.of("centre",wood.toShortString(),"wideTrunks",wide,"tallestTrunk",tallestTree,"logs",logs,"leaves",leaves,"podzol",podzol));
var spikes=f.sample(NorthTerrain202.Surface.SPIKES);int iceAbove=0,tallest=0;
for(int dx=-40;dx<=40;dx++)for(int dz=-40;dz<=40;dz++){
int x=spikes.getX()+dx,z=spikes.getZ()+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);
for(int y=(int)Math.ceil(c.top())+3;y<c.top()+64;y++)if(chunk.getBlockState(new BlockPos(x,y,z)).is(Blocks.PACKED_ICE)){iceAbove++;tallest=Math.max(tallest,y-(int)c.top());}
}
require(iceAbove>100&&tallest>=7,"Vanilla ice spikes absent: "+iceAbove+" / "+tallest);
result.put("nativeIceSpikes",Map.of("centre",spikes.toShortString(),"iceAboveTerrain",iceAbove,"tallest",tallest));
int snow=0,checked=0;
for(int dx=-36;dx<=36;dx+=3)for(int dz=-36;dz<=36;dz+=3){
int x=village.arrival().getX()+dx,z=village.arrival().getZ()+dz;if(village.nearHouse(x,z))continue;
require(f.snowyPlain(x,z),"Igloos leave the snow meadow");checked++;
int top=(int)Math.floor(f.column(x,z).top());
for(int y=top-2;y<=top+3;y++)if(level.getBlockState(new BlockPos(x,y,z)).is(Blocks.SNOW_BLOCK)||level.getBlockState(new BlockPos(x,y,z)).is(Blocks.SNOW)){snow++;break;}
}
require(checked>100&&snow>checked*.90,"Village not covered by snow: "+snow+" / "+checked);
result.put("snowyVillageGround",Map.of("checked",checked,"snow",snow));
int rock=0,exteriorIce=0;
for(int n=0;n<16;n++){
double a=n*Math.PI/8;int x=island.centerX()+(int)(Math.sin(a)*island.radius()*.9),z=island.centerZ()+(int)(Math.cos(a)*island.radius()*.9);var c=f.column(x,z);
var chunk=level.getChunk(x>>4,z>>4);
for(int y=(int)c.bottom()-6;y<c.top()-8;y++){
var state=chunk.getBlockState(new BlockPos(x,y,z));if(state.is(Blocks.STONE)||state.is(Blocks.DEEPSLATE))rock++;
if(state.is(Blocks.BLUE_ICE)||state.is(Blocks.PACKED_ICE))exteriorIce++;
}
}
require(rock>100&&exteriorIce==0,"Exterior ice coating: "+exteriorIce);
result.put("exteriorRock",Map.of("rock",rock,"ice",exteriorIce));
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,62 @@
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;
/** Inspect actual native vegetation, snow and exposed rock in a fresh 203 world. */
final class North203Checks {
static void verify(ServerLevel level,ExpansionIsland island,NorthVillage200.Plan village,Map<String,Object> result){
var f=NorthTerrain203.field(island);var wood=f.sample(NorthTerrain203.Surface.OLD_GROWTH);
int logs=0,leaves=0,podzol=0,tallestTree=0;var trunks=new HashSet<Long>();
for(int dx=-32;dx<=32;dx++)for(int dz=-32;dz<=32;dz++){
int x=wood.getX()+dx,z=wood.getZ()+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);int trunk=0;
for(int y=(int)c.top()-2;y<=c.top()+68;y++){
var state=chunk.getBlockState(new BlockPos(x,y,z));
if(state.is(Blocks.SPRUCE_LOG)){logs++;trunk++;trunks.add(((long)x<<32)^(z&0xffffffffL));}
if(state.is(Blocks.SPRUCE_LEAVES))leaves++;
if(state.is(Blocks.PODZOL))podzol++;
}
tallestTree=Math.max(tallestTree,trunk);
}
int wide=0;for(long key:trunks){int x=(int)(key>>32),z=(int)key;if(trunks.contains(((long)(x+1)<<32)^(z&0xffffffffL))&&trunks.contains(((long)x<<32)^((z+1)&0xffffffffL))&&trunks.contains(((long)(x+1)<<32)^((z+1)&0xffffffffL)))wide++;}
require(wide>0&&tallestTree>=40&&tallestTree<=64&&logs>100&&leaves>100&&podzol>100,"Native giant spruce absent: "+wide+" wide, "+tallestTree+" tall, "+logs+" logs");
result.put("nativeGiantTaiga",Map.of("centre",wood.toShortString(),"wideTrunks",wide,"tallestTrunk",tallestTree,"logs",logs,"leaves",leaves,"podzol",podzol));
var spikes=f.sample(NorthTerrain203.Surface.SPIKES);int iceAbove=0,tallest=0;var materials=new HashMap<String,Integer>();
for(int dx=-40;dx<=40;dx++)for(int dz=-40;dz<=40;dz++){
int x=spikes.getX()+dx,z=spikes.getZ()+dz;var c=f.column(x,z);var chunk=level.getChunk(x>>4,z>>4);
for(int y=(int)Math.floor(c.top())-4;y<=c.top();y++){
var b=chunk.getBlockState(new BlockPos(x,y,z)).getBlock();
if(b==Blocks.STONE||b==Blocks.ICE||b==Blocks.PACKED_ICE||b==Blocks.BLUE_ICE||b==Blocks.SNOW_BLOCK)
materials.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(b).toString(),1,Integer::sum);
}
for(int y=(int)Math.ceil(c.top())+3;y<c.top()+64;y++)if(chunk.getBlockState(new BlockPos(x,y,z)).is(Blocks.PACKED_ICE)){iceAbove++;tallest=Math.max(tallest,y-(int)c.top());}
}
require(iceAbove>100&&tallest>=7,"Vanilla ice spikes absent: "+iceAbove+" / "+tallest);
require(materials.size()==5,"Missing summit material: "+materials);result.put("summitMaterials",materials);
result.put("nativeIceSpikes",Map.of("centre",spikes.toShortString(),"iceAboveTerrain",iceAbove,"tallest",tallest));
int layers=0,checked=0;
for(int dx=-36;dx<=36;dx+=3)for(int dz=-36;dz<=36;dz+=3){
int x=village.arrival().getX()+dx,z=village.arrival().getZ()+dz;if(village.nearHouse(x,z))continue;
require(f.snowyPlain(x,z),"Igloos leave their meadow");checked++;
int top=(int)Math.floor(f.column(x,z).top());
for(int y=top-2;y<=top+3;y++)if(level.getBlockState(new BlockPos(x,y,z)).is(Blocks.SNOW)){layers++;break;}
}
require(checked>100&&layers==0,"Additive snow returned: "+layers);
result.put("meadowWithoutSnowPass",Map.of("checked",checked,"snowLayers",layers));
int rock=0,exteriorIce=0;
for(int n=0;n<16;n++){
double a=n*Math.PI/8;int x=island.centerX()+(int)(Math.sin(a)*island.radius()*.9),z=island.centerZ()+(int)(Math.cos(a)*island.radius()*.9);var c=f.column(x,z);
var chunk=level.getChunk(x>>4,z>>4);
for(int y=(int)c.bottom()-6;y<c.top()-8;y++){
var state=chunk.getBlockState(new BlockPos(x,y,z));if(state.is(Blocks.STONE)||state.is(Blocks.DEEPSLATE))rock++;
if(state.is(Blocks.BLUE_ICE)||state.is(Blocks.PACKED_ICE))exteriorIce++;
}
}
require(rock>100&&exteriorIce==0,"Exterior ice coating: "+exteriorIce);
result.put("exteriorRock",Map.of("rock",rock,"ice",exteriorIce));
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,113 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.templatesystem.*;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
/** One village, five terrain-fitted homes, exactly one native curing cellar. Chunk-clipped and deterministic. */
public final class NorthVillage200 {
private NorthVillage200() {}
public record House(BlockPos origin,Rotation rotation,boolean cellar) {
public BlockPos at(int x,int y,int z){return origin.offset(new BlockPos(x,y,z).rotate(rotation));}
}
public record Plan(BlockPos arrival,BlockPos relay,List<House> houses) {
public boolean nearHouse(int x,int z){return houses.stream().anyMatch(h->Math.hypot(x-h.at(3,0,3).getX(),z-h.at(3,0,3).getZ())<13);}
}
public static Plan plan(ExpansionIsland island){
if(NorthTerrain203.active(island))return NorthVillage203.plan(island);
if(NorthTerrain202.active(island))return NorthVillage202.plan(island);
var f=NorthTerrain.field(island);
int cx=island.centerX(),cz=island.centerZ()+(int)(island.radius()*.52);
boolean compact=NorthTerrain201.active(island);
if(compact){var lake=f.lakes().getFirst();cx=lake.x()+55;cz=lake.z()+120;}
double best=Double.MAX_VALUE;int vx=cx,vz=cz;
// Fit a neighbourhood, not just a centre column. No terrain excavation to impose a flat town.
int spanX=compact?64:240,spanZ=compact?32:144,step=compact?8:24;
for(int dx=-spanX;dx<=spanX;dx+=step)for(int dz=-spanZ;dz<=spanZ;dz+=step){
int x=cx+dx,z=cz+dz;double low=999,high=0;boolean wet=false;
for(int a=-32;a<=32;a+=16)for(int b=-32;b<=32;b+=16){
var c=f.column(x+a,z+b);low=Math.min(low,c.top());high=Math.max(high,c.top());wet|=c.water()>=0||c.edge()<120;
}
double score=(high-low)*3+Math.max(0,high-250)+Math.hypot(dx,dz)*.015;
if(!wet&&score<best){best=score;vx=x;vz=z;}
}
var houses=new ArrayList<House>();
for(int n=0;n<5;n++){
double angle=n*Math.PI*2/5+.3;
int x=vx+(int)(Math.sin(angle)*27),z=vz+(int)(Math.cos(angle)*27);
Rotation rotation=Rotation.values()[Math.floorMod((int)(island.seed()+n),4)];
int floor=(int)Math.ceil(f.column(x,z).top());
var offset=new BlockPos(3,0,3).rotate(rotation);
houses.add(new House(new BlockPos(x-offset.getX(),floor,z-offset.getZ()),rotation,n==0));
}
var arrival=new BlockPos(vx,(int)Math.ceil(f.column(vx,vz).top())+2,vz);
// Continuation is beyond the village, on the opposite side of the continent.
int rx=island.centerX(),rz=island.centerZ()-(int)(island.radius()*.58);
best=Double.MAX_VALUE;BlockPos relay=null;
for(int dx=-96;dx<=96;dx+=8)for(int dz=-48;dz<=48;dz+=8){
var c=f.column(rx+dx,rz+dz);double score=f.slope(rx+dx,rz+dz)*40+Math.hypot(dx,dz)*.1;
if(c.water()<0&&score<best){best=score;relay=new BlockPos(rx+dx,(int)Math.ceil(c.top())+1,rz+dz);}
}
if(relay==null)throw new IllegalStateException("North relay has no ground");
return new Plan(arrival,relay,List.copyOf(houses));
}
public static boolean rejectTree(ChunkGenerator generator,BlockPos pos){
var adapter=LabExpansions199.get(generator);if(adapter==null)return false;
var i=adapter.regionAt(pos.getX(),pos.getZ());if(!NorthTerrain.active(i))return false;
return adapter.village(i).nearHouse(pos.getX(),pos.getZ())||NorthTerrain.field(i).column(pos.getX(),pos.getZ()).water()>=0;
}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk){
var adapter=LabExpansions199.get(generator);if(adapter==null)return;
var island=adapter.regionAt(chunk.getPos().getMiddleBlockX(),chunk.getPos().getMiddleBlockZ());
if(!NorthTerrain.active(island))return;
var plan=adapter.village(island);var cp=chunk.getPos();
var box=new BoundingBox(cp.getMinBlockX(),level.getMinY(),cp.getMinBlockZ(),cp.getMaxBlockX(),level.getMaxY()-1,cp.getMaxBlockZ());
for(var house:plan.houses()){
var centre=house.at(3,0,3);
if(centre.getX()<cp.getMinBlockX()-12||centre.getX()>cp.getMaxBlockX()+12||centre.getZ()<cp.getMinBlockZ()-12||centre.getZ()>cp.getMaxBlockZ()+12)continue;
var f=NorthTerrain.field(island);
for(int x=-1;x<=7;x++)for(int z=-1;z<=8;z++){
var p=house.at(x,0,z);if(!box.isInside(p))continue;
int ground=(int)Math.floor(f.column(p.getX(),p.getZ()).top());
for(int y=Math.min(ground-3,p.getY()-2);y<p.getY();y++)level.setBlock(new BlockPos(p.getX(),y,p.getZ()),(y==p.getY()-1?Blocks.SNOW_BLOCK:Blocks.STONE).defaultBlockState(),Block.UPDATE_CLIENTS);
for(int y=0;y<=7;y++)level.setBlock(p.above(y),Blocks.AIR.defaultBlockState(),Block.UPDATE_CLIENTS);
}
if(house.cellar()){
template(level,box,house,"bottom",0,-18,-2,island.seed());
for(int depth=3;depth<=12;depth+=3)template(level,box,house,"middle",2,-depth,4,island.seed());
var marker=house.at(1,-16,4);
if(box.isInside(marker))level.setBlock(marker,Blocks.AIR.defaultBlockState(),Block.UPDATE_CLIENTS);
var chest=house.at(1,-17,4);
if(box.isInside(chest)&&level.getBlockEntity(chest) instanceof ChestBlockEntity loot)
loot.setLootTable(BuiltInLootTables.IGLOO_CHEST,island.seed()^chest.asLong());
}
template(level,box,house,"top",0,0,0,island.seed());
if(house.cellar()){
// Finish the shared seam after the top template's neighbour updates.
var rung=Blocks.LADDER.defaultBlockState().setValue(LadderBlock.FACING,net.minecraft.core.Direction.NORTH).rotate(house.rotation());
for(int y=-1;y>=-16;y--){var p=house.at(3,y,5);if(box.isInside(p))level.setBlock(p,rung,Block.UPDATE_CLIENTS);}
}
if(!house.cellar()){
var trapdoor=house.at(3,0,5);
if(box.isInside(trapdoor))level.setBlock(trapdoor,Blocks.SNOW_BLOCK.defaultBlockState(),Block.UPDATE_CLIENTS);
}
}
}
private static void template(WorldGenLevel level,BoundingBox box,House house,String name,int x,int y,int z,long seed){
var origin=house.at(x,y,z);
var template=level.getLevel().getStructureTemplateManager().getOrCreate(Identifier.withDefaultNamespace("igloo/"+name));
var settings=new StructurePlaceSettings().setRotation(house.rotation()).setBoundingBox(box)
.setIgnoreEntities(false).addProcessor(BlockIgnoreProcessor.STRUCTURE_BLOCK)
.setLiquidSettings(LiquidSettings.IGNORE_WATERLOGGING);
template.placeInWorld(level,origin,origin,settings,RandomSource.create(seed^origin.asLong()),Block.UPDATE_CLIENTS);
}
}
@@ -0,0 +1,26 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Rotation;
/** Select a whole snow meadow before placing any igloo; never paint isolated white pads on bare ground. */
final class NorthVillage202 {
static NorthVillage200.Plan plan(ExpansionIsland island){
var f=NorthTerrain202.field(island);var centre=f.villageSite();var houses=new ArrayList<NorthVillage200.House>();
for(int n=0;n<5;n++){
double angle=n*Math.PI*2/5+.3;int x=centre.getX()+(int)(Math.sin(angle)*27),z=centre.getZ()+(int)(Math.cos(angle)*27);
var rotation=Rotation.values()[Math.floorMod((int)(island.seed()+n),4)];
var offset=new BlockPos(3,0,3).rotate(rotation);
houses.add(new NorthVillage200.House(new BlockPos(x-offset.getX(),(int)Math.ceil(f.column(x,z).top()),z-offset.getZ()),rotation,n==0));
}
int rx=island.centerX(),rz=island.centerZ()-(int)(island.radius()*.58);double best=Double.MAX_VALUE;BlockPos relay=null;
for(int dx=-96;dx<=96;dx+=8)for(int dz=-48;dz<=48;dz+=8){
int x=rx+dx,z=rz+dz;var c=f.column(x,z);double score=f.slope(x,z)*40+Math.hypot(dx,dz)*.1;
if(c.water()<0&&!houses.stream().anyMatch(h->Math.hypot(h.at(3,0,3).getX()-x,h.at(3,0,3).getZ()-z)<18)&&score<best){best=score;relay=new BlockPos(x,(int)Math.ceil(c.top())+1,z);}
}
if(relay==null)throw new IllegalStateException("No north continuation site");
return new NorthVillage200.Plan(centre,relay,List.copyOf(houses));
}
}
@@ -0,0 +1,26 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Rotation;
/** Keep the existing five homes on a quiet cold meadow; no additional settlement. */
final class NorthVillage203 {
static NorthVillage200.Plan plan(ExpansionIsland island){
var f=NorthTerrain203.field(island);var centre=f.villageSite();var houses=new ArrayList<NorthVillage200.House>();
for(int n=0;n<5;n++){
double angle=n*Math.PI*2/5+.3;int x=centre.getX()+(int)(Math.sin(angle)*27),z=centre.getZ()+(int)(Math.cos(angle)*27);
var rotation=Rotation.values()[Math.floorMod((int)(island.seed()+n),4)];
var offset=new BlockPos(3,0,3).rotate(rotation);
houses.add(new NorthVillage200.House(new BlockPos(x-offset.getX(),(int)Math.ceil(f.column(x,z).top()),z-offset.getZ()),rotation,n==0));
}
int rx=island.centerX(),rz=island.centerZ()-(int)(island.radius()*.58);double best=Double.MAX_VALUE;BlockPos relay=null;
for(int dx=-96;dx<=96;dx+=8)for(int dz=-48;dz<=48;dz+=8){
int x=rx+dx,z=rz+dz;var c=f.column(x,z);double score=f.slope(x,z)*40+Math.hypot(dx,dz)*.1;
if(c.water()<0&&!houses.stream().anyMatch(h->Math.hypot(h.at(3,0,3).getX()-x,h.at(3,0,3).getZ()-z)<18)&&score<best){best=score;relay=new BlockPos(x,(int)Math.ceil(c.top())+1,z);}
}
if(relay==null)throw new IllegalStateException("No north continuation site");
return new NorthVillage200.Plan(centre,relay,List.copyOf(houses));
}
}
@@ -0,0 +1,28 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.*;
/** Verify against the registered pre-terrace preset, not a reconstructed approximation. */
final class Original191 {
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var g=(NoiseBasedChunkGenerator)level.getChunkSource().getGenerator();
var expected=level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS,Identifier.fromNamespaceAndPath("sanctuary_test","living_details_v1_10"))).value();
var reference=RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE),level.getSeed(),expected);
var actual=level.getChunkSource().randomState();int samples=0,columns=0;
if(s.field().terraces()||s.field().refined())throw new IllegalStateException("Terrain modifier still enabled");
for(int x=-320;x<=320;x+=16)for(int z=-320;z<=320;z+=16){
if(Terrain189.isExposed(s,x,z))throw new IllegalStateException("Added outcrop still enabled");columns++;
for(int y=0;y<640;y+=8){
float a=actual.sampleBlockValueUncached(g.generatorSettings().value().noiseRouter().finalDensity(),x,y,z);
float b=reference.sampleBlockValueUncached(expected.noiseRouter().finalDensity(),x,y,z);
if(Float.floatToIntBits(a)!=Float.floatToIntBits(b))throw new IllegalStateException("Pre-terrace density mismatch "+x+","+y+","+z);
samples++;
}
}
return Map.of("referencePreset","living_details_v1_10","exactDensityMatches",samples,"columnsWithoutAddedOutcrops",columns,"heightRemap",false,"addedSurfaceStone",false);
}
}
@@ -0,0 +1,83 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Search all nearby mouths, then carve only the shortest broad natural spillway. */
final class Outlets190 {
record Lane(List<BlockPos> cells,BlockPos lip,int drop) {}
record Mouth(BlockPos source,List<Lane> lanes,int length,int cut,int score) {}
record Result(List<Water189.Outlet> outlets,int carved,int candidates) {}
private static long column(BlockPos p){return ((long)p.getX()<<32)^(p.getZ()&0xffffffffL);}
private static boolean rock(AdventureBiomes180 s,Map<BlockPos,Boolean> cache,BlockPos p){
return cache.computeIfAbsent(p,k->s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,k.getX(),k.getY(),k.getZ())>0);
}
static Result create(AdventureBiomes180 s,List<SurfaceWater187.Pool> pools,Map<BlockPos,BlockState> blocks,Map<Long,Integer> columns){
var outlets=new ArrayList<Water189.Outlet>();int carved=0,candidates=0,mouths=0;
var cache=new HashMap<BlockPos,Boolean>();
for(var pool:pools){
if(mouths>=2)break; // At most two five-block mouths.
Mouth best=null;
var edge=pool.cells().stream().filter(p->p.getY()==pool.surface()).sorted(Comparator.<BlockPos>comparingInt(p->p.getX()).thenComparingInt(p->p.getZ())).toList();
for(var edgeCell:edge)for(var d:Direction.Plane.HORIZONTAL){
if(pool.cells().contains(edgeCell.relative(d)))continue;
for(int width:new int[]{5,4}){
var start=edgeCell.relative(d,-2);var side=d.getClockWise();var lanes=new ArrayList<Lane>();
int cut=0,longest=0,totalLength=0;boolean valid=true;
for(int offset=-2;offset<width-2&&valid;offset++){
var from=start.relative(side,offset);
if(!pool.cells().contains(from)){valid=false;break;}
var cells=new ArrayList<BlockPos>();BlockPos lip=null;int drop=0;
for(int n=1;n<=10;n++){
var q=from.relative(d,n);
if(rock(s,cache,q.above(3))||Dungeon180.near(s,q.getX(),q.getZ(),24)||Math.hypot(q.getX(),q.getZ())<70){valid=false;break;}
for(int h=0;h<=2;h++)if(rock(s,cache,q.above(h)))cut++;
if(cut>80){valid=false;break;}
cells.add(q);
if(!rock(s,cache,q.below())&&!pool.cells().contains(q.below())){
// An underground air pocket is not a cliff outlet. Check the actual
// column, not only the nearest cached four-block surface sample.
if(s.polish197() && !Details197.openCliff(s,q,d)){valid=false;break;}
while(drop<64&&!rock(s,cache,q.below(drop+1)))drop++;
if(drop<8){valid=false;break;}
lip=q;longest=Math.max(longest,n);totalLength+=n;break;
}
// A bank can open at the lip, but not halfway along the channel.
if(!pool.cells().contains(q)&&(offset==-2||offset==width-3)){
var bank=q.relative(side,offset<0?-1:1);
if(!rock(s,cache,bank)&&!pool.cells().contains(bank)){valid=false;break;}
}
}
if(lip==null)valid=false;
else lanes.add(new Lane(List.copyOf(cells),lip,drop));
}
if(!valid)continue;candidates++;
// Distance wins over scan order. Small preference for five blocks and little excavation.
int score=longest*100+totalLength*5+cut*2+(5-width)*35;
var mouth=new Mouth(start,List.copyOf(lanes),longest,cut,score);
if(best==null||mouth.score<best.score)best=mouth;
}
}
if(best==null)continue;mouths++;
for(var lane:best.lanes){
outlets.add(new Water189.Outlet(lane.cells.getFirst().relative(direction(lane.cells.getFirst(),lane.lip),-1),lane.lip,lane.cells.size(),lane.drop));
for(var p:lane.cells){
for(int h=0;h<=2;h++){
var q=p.above(h);if(rock(s,cache,q))carved++;
blocks.put(q,(h==0?Blocks.WATER:Blocks.AIR).defaultBlockState());
}
columns.put(column(p),p.getY());
}
for(int h=1;h<=lane.drop;h++)blocks.put(lane.lip.below(h),Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL,8));
}
}
return new Result(List.copyOf(outlets),carved,candidates);
}
private static Direction direction(BlockPos start,BlockPos end){
if(end.getX()!=start.getX())return end.getX()>start.getX()?Direction.EAST:Direction.WEST;
return end.getZ()>start.getZ()?Direction.SOUTH:Direction.NORTH;
}
}
@@ -0,0 +1,137 @@
package fr.koka.sanctuarytest;
import java.util.*;
import fr.koka.sanctuary.palace.OriginBlock;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
/** A small origin rosette grows into a climbable stem and an eight-petalled glass crown. */
final class Palace184 {
static final int ROOT=640, FLOOR=1278, CROWN_BOTTOM=1160;
record Step(BlockPos floor,Direction facing,boolean stair) {}
record Plan(Map<BlockPos,BlockState> blocks,List<Step> steps) {}
private Palace184() {}
private static void put(Map<BlockPos,BlockState> m,int x,int y,int z,Block b){m.put(new BlockPos(x,y,z),b.defaultBlockState());}
private static double axisDistance(int x,int z){return Math.min(Math.min(Math.abs(x),Math.abs(z)),Math.abs(Math.abs(x)-Math.abs(z))*.7071);}
private static List<BlockPos> circuit(){
int[][] corners={{12,0},{12,5},{5,12},{-5,12},{-12,5},{-12,-5},{-5,-12},{5,-12},{12,-5},{12,0}};
var route=new ArrayList<BlockPos>();int x=12,z=0;
for(int k=1;k<corners.length;k++)while(x!=corners[k][0]||z!=corners[k][1]){
if(x!=corners[k][0]){route.add(new BlockPos(x,0,z));x+=Integer.signum(corners[k][0]-x);}
if(z!=corners[k][1]){route.add(new BlockPos(x,0,z));z+=Integer.signum(corners[k][1]-z);}
}
return route;
}
static Plan plan(long seed,Station183.Layout layout){
var m=new LinkedHashMap<BlockPos,BlockState>();
Rosette183.plan(seed).forEach((p,b)->m.put(p.above(320),b));
// Slim mineral stalk, striped by eight winding calcite veins; no opaque broad slab overhead.
for(int y=652;y<FLOOR;y++){
double phase=(y-652)*.008;int cx=(int)Math.round(1.3*Math.sin(phase)),cz=(int)Math.round(1.3*Math.cos(phase));
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)if(x*x+z*z<=8)
put(m,x+cx,y,z+cz,(x*x+z*z<=2)?Blocks.DEEPSLATE:Math.floorMod(x+z+y,13)==0?Blocks.MOSS_BLOCK:Blocks.TUFF);
for(int k=0;k<8;k++){
double a=(k+.5)*Math.PI/4+phase;
put(m,cx+(int)Math.round(3*Math.cos(a)),y,cz+(int)Math.round(3*Math.sin(a)),y%48==0?Blocks.SEA_LANTERN:Blocks.CALCITE);
}
}
// Umbrella ribs flare out gradually from the stalk. Every consecutive voxel is connected.
for(int k=0;k<8;k++){
double a=(k+.5)*Math.PI/4;
for(int y=CROWN_BOTTOM;y<=FLOOR;y++){
double t=(y-CROWN_BOTTOM)/(double)(FLOOR-CROWN_BOTTOM),r=3+24*t*t;
int x=(int)Math.round(Math.sin(a)*r),z=-(int)Math.round(Math.cos(a)*r);
for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
put(m,x+dx,y,z+dz,(dx==0&&dz==0)?Blocks.CALCITE:Blocks.TUFF);
}
}
// Crown floor: stone paths, open coloured glass petals, scalloped balconies.
int radius=Math.max(29,layout.half()+14);
for(int x=-radius-2;x<=radius+2;x++)for(int z=-radius-2;z<=radius+2;z++){
double r=Math.hypot(x,z),a=Math.atan2(x,-z),edge=radius-2+2*Math.cos(8*a);
if(r>edge)continue;
boolean stone=axisDistance(x,z)<1.5||r<layout.half()+1||r>=9&&r<=12||r>edge-1;
put(m,x,FLOOR,z,stone?(r>edge-1?Blocks.CALCITE:Blocks.POLISHED_TUFF):Rosette183.GLASS[Rosette183.sector(x,z)]);
if(stone)put(m,x,FLOOR-1,z,r>edge-1?Blocks.TUFF:Blocks.CHISELED_TUFF);
if(r>edge-1&&axisDistance(x,z)>1.5)put(m,x,FLOOR+1,z,Blocks.MOSSY_COBBLESTONE_WALL);
// Wide chamber under a transparent vault; eight doorways stay on the compass axes.
if(r>=22&&r<=23&&axisDistance(x,z)>1.5)for(int h=2;h<=8;h++)put(m,x,FLOOR+h,z,Rosette183.GLASS[Rosette183.sector(x,z)]);
for(int h=8;h<=32;h++){
double domeR=23*Math.sqrt(Math.max(0,1-Math.pow((h-8)/24.0,2)));
double nextR=23*Math.sqrt(Math.max(0,1-Math.pow((h-7)/24.0,2)));
if(r<=domeR&&(r>nextR||r>domeR-1))put(m,x,FLOOR+h,z,r<2?Blocks.STAINED_GLASS.white():Rosette183.GLASS[Rosette183.sector(x,z)]);
}
}
for(int k=0;k<8;k++){
double a=(k+.5)*Math.PI/4;
for(int h=0;h<=32;h++){
double r=h<8?23:23*Math.sqrt(Math.max(0,1-Math.pow((h-8)/24.0,2)));
int x=(int)Math.round(Math.sin(a)*r),z=-(int)Math.round(Math.cos(a)*r);
put(m,x,FLOOR+h,z,h<4?Blocks.CALCITE:Blocks.CUT_COPPER.waxed().oxidized());
}
}
// Horizontal upper portal suspended over the atlas; unlike the lower 5x5 sunken portal.
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)if(Math.abs(x)==3||Math.abs(z)==3)
put(m,x,FLOOR+9,z,Math.abs(x)==3&&Math.abs(z)==3?Blocks.CRYING_OBSIDIAN:Blocks.OBSIDIAN);
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)put(m,x,FLOOR,z,Blocks.CALCITE);
// A continuous three-wide stair goes around the stalk. Build its clearance last so ribs
// and viewing balconies cannot sever the climb. All writes remain generation-only.
var route=circuit();var routeColumns=new HashSet<>(route);var steps=new ArrayList<Step>();
for(int i=0,y=ROOT-2;y<=FLOOR;i++,y=ROOT-2+i/3){
var p=route.get(i%route.size());var next=route.get((i+1)%route.size());
int dx=next.getX()-p.getX(),dz=next.getZ()-p.getZ();Direction facing=dx>0?Direction.EAST:dx<0?Direction.WEST:dz>0?Direction.SOUTH:Direction.NORTH;
var at=new BlockPos(p.getX(),y,p.getZ());boolean stair=i%3==2;steps.add(new Step(at,facing,stair));
// Five broad leaves punctuate the climb without becoming solid rings around it.
if(i%384==0&&y<FLOOR)for(int x=-5;x<=5;x++)for(int z=-5;z<=5;z++)if(x*x+z*z<=25){
int bx=p.getX()+x,bz=p.getZ()+z;
put(m,bx,y,bz,(x*x+z*z>18)?Blocks.MOSS_BLOCK:Blocks.TUFF);
if(x*x+z*z>20)put(m,bx,y-1,bz,Blocks.CHISELED_TUFF);
}
for(int w=-1;w<=1;w++){
var q=at.offset(-dz*w,0,dx*w);
if(w!=0&&routeColumns.contains(new BlockPos(q.getX(),0,q.getZ())))continue;
m.put(q,stair?Blocks.TUFF_BRICK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING,facing):Blocks.TUFF_BRICKS.defaultBlockState());
}
if(!routeColumns.contains(new BlockPos(p.getX()-dz*2,0,p.getZ()+dx*2)))put(m,p.getX()-dz*2,y+1,p.getZ()+dx*2,Blocks.MOSSY_COBBLESTONE_WALL);
}
for(var step:steps)for(int w=-1;w<=1;w++){
int dx=step.facing().getStepX(),dz=step.facing().getStepZ();var q=step.floor().offset(-dz*w,0,dx*w);
if(w!=0&&routeColumns.contains(new BlockPos(q.getX(),0,q.getZ())))continue;
m.put(q.above(),Blocks.AIR.defaultBlockState());m.put(q.above(2),Blocks.AIR.defaultBlockState());m.put(q.above(3),Blocks.AIR.defaultBlockState());
}
// Root entrance connects the rosette's east axis to the first stair tread.
for(int x=8;x<=12;x++)for(int z=-1;z<=1;z++){put(m,x,ROOT-2,z,Blocks.TUFF_BRICKS);for(int h=1;h<=3;h++)put(m,x,ROOT-2+h,z,Blocks.AIR);}
// The stem stops below the chart. Keep the centre of the entire room and portal hollow.
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)
for(int h=1;h<=8;h++)put(m,x,FLOOR+h,z,Blocks.AIR);
return new Plan(Collections.unmodifiableMap(m),List.copyOf(steps));
}
static Map<String,Object> check(ServerLevel level,Plan plan){
if(level.getHeight()!=1600)throw new IllegalStateException("Ascent requires a new 1600-high dimension");
for(var e:plan.blocks().entrySet())if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("Palace mismatch "+e.getKey());
var core=new BlockPos(0,ROOT,0);
if(!level.getBlockState(core).is(OriginBlock.BLOCK)||level.getBlockState(Origin181.CORE).is(OriginBlock.BLOCK))throw new IllegalStateException("Wrong origin altitude");
Step before=null;
for(var step:plan.steps()){
var p=step.floor();
if(level.getBlockState(p).getCollisionShape(level,p).isEmpty()||!level.getBlockState(p.above()).isAir()||!level.getBlockState(p.above(2)).isAir())throw new IllegalStateException("Climb interrupted "+p);
if(before!=null&&(Math.abs(p.getX()-before.floor().getX())+Math.abs(p.getZ()-before.floor().getZ())!=1||p.getY()-before.floor().getY()<0||p.getY()-before.floor().getY()>1))throw new IllegalStateException("Nonwalkable stair connection");
before=step;
}
int rim=0;for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++){
var b=level.getBlockState(new BlockPos(x,FLOOR+9,z));
if(Math.abs(x)==3||Math.abs(z)==3){if(!b.is(Blocks.OBSIDIAN)&&!b.is(Blocks.CRYING_OBSIDIAN))throw new IllegalStateException("Upper portal rim");rim++;}
else if(!b.isAir())throw new IllegalStateException("Upper portal not horizontal/hollow");
}
// Even the largest supported shader shadow volume cannot reach the crown from the island.
if(CROWN_BOTTOM<=316+3*fr.koka.sanctuary.client.shader.ShadowTerrain.MAX_DISTANCE+8)throw new IllegalStateException("Crown can cast surface shadow");
for(int radius:new int[]{288,395,544}){
var layout=Station183.layout(radius);var variant=plan(42,layout);
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)if(!variant.blocks().get(new BlockPos(x,FLOOR,z)).is(Blocks.CALCITE))throw new IllegalStateException("Unsupported atlas size");
}
return Map.of("origin",core.toShortString(),"rootFloor",ROOT-2,"palaceFloor",FLOOR,"roof",FLOOR+32,"stairTreads",plan.steps().size(),"blocks",plan.blocks().size(),"upperPortalRim",rim,"upperPortalOpening","5x5 horizontal","noiseHeight",640,"dimensionHeight",1600);
}
}
@@ -0,0 +1,127 @@
package fr.koka.sanctuarytest;
import java.util.*;
import fr.koka.sanctuary.palace.OriginBlock;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
/** A one-block visual guide to the retained flower palace; arrival is by flying boat. */
final class Palace185 {
static final int ROOT=Palace184.ROOT, FLOOR=Palace184.FLOOR, CROWN_BOTTOM=Palace184.CROWN_BOTTOM, PORTAL_Y=FLOOR+31;
private Palace185() {}
private static void put(Map<BlockPos,BlockState> m,int x,int y,int z,Block b){m.put(new BlockPos(x,y,z),b.defaultBlockState());}
private static double axisDistance(int x,int z){return Math.min(Math.min(Math.abs(x),Math.abs(z)),Math.abs(Math.abs(x)-Math.abs(z))*.7071);}
static Map<BlockPos,BlockState> plan(long seed,Station183.Layout layout){
var m=new LinkedHashMap<BlockPos,BlockState>();
Rosette183.plan(seed).forEach((p,b)->m.put(p.above(320),b));
for(int y=652;y<FLOOR;y++)put(m,0,y,0,Blocks.CALCITE);
// Start at the single central voxel, then gradually spread into the approved upper ribs.
for(int k=0;k<8;k++){
double a=(k+.5)*Math.PI/4;
for(int y=CROWN_BOTTOM;y<=FLOOR;y++){
double t=(y-CROWN_BOTTOM)/(double)(FLOOR-CROWN_BOTTOM),r=27*t*t;
int x=(int)Math.round(Math.sin(a)*r),z=-(int)Math.round(Math.cos(a)*r);
int width=t<.25?0:1;
for(int dx=-width;dx<=width;dx++)for(int dz=-width;dz<=width;dz++)
put(m,x+dx,y,z+dz,(dx==0&&dz==0)?Blocks.CALCITE:Blocks.TUFF);
}
}
// Crown floor: stone paths, open coloured glass petals, scalloped balconies.
int radius=Math.max(29,layout.half()+14);
for(int x=-radius-2;x<=radius+2;x++)for(int z=-radius-2;z<=radius+2;z++){
double r=Math.hypot(x,z),a=Math.atan2(x,-z),edge=radius-2+2*Math.cos(8*a);
if(r>edge)continue;
boolean stone=axisDistance(x,z)<1.5||r<layout.half()+1||r>=9&&r<=12||r>edge-1;
put(m,x,FLOOR,z,stone?(r>edge-1?Blocks.CALCITE:Blocks.POLISHED_TUFF):Rosette183.GLASS[Rosette183.sector(x,z)]);
if(stone)put(m,x,FLOOR-1,z,r>edge-1?Blocks.TUFF:Blocks.CHISELED_TUFF);
if(r>edge-1&&axisDistance(x,z)>1.5)put(m,x,FLOOR+1,z,Blocks.MOSSY_COBBLESTONE_WALL);
// Wide chamber under a transparent vault; eight doorways stay on the compass axes.
if(r>=22&&r<=23&&axisDistance(x,z)>1.5){
put(m,x,FLOOR+1,z,Blocks.CALCITE);
for(int h=2;h<=8;h++)put(m,x,FLOOR+h,z,Rosette183.GLASS[Rosette183.sector(x,z)]);
}
for(int h=8;h<=32;h++){
double domeR=23*Math.sqrt(Math.max(0,1-Math.pow((h-8)/24.0,2)));
double nextR=23*Math.sqrt(Math.max(0,1-Math.pow((h-7)/24.0,2)));
if(r<=domeR&&(r>nextR||r>domeR-1))put(m,x,FLOOR+h,z,r<2?Blocks.STAINED_GLASS.white():Rosette183.GLASS[Rosette183.sector(x,z)]);
}
}
for(int k=0;k<8;k++){
double a=(k+.5)*Math.PI/4;
for(int h=0;h<=32;h++){
double r=h<8?23:23*Math.sqrt(Math.max(0,1-Math.pow((h-8)/24.0,2)));
int x=(int)Math.round(Math.sin(a)*r),z=-(int)Math.round(Math.cos(a)*r);
put(m,x,FLOOR+h,z,h<4?Blocks.CALCITE:Blocks.CUT_COPPER.waxed().oxidized());
}
}
// The horizontal frame replaces the summit glazing; the 5x5 shaft is open to the sky.
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++){
if(Math.abs(x)==3||Math.abs(z)==3)
put(m,x,PORTAL_Y,z,Math.abs(x)==3&&Math.abs(z)==3?Blocks.CRYING_OBSIDIAN:Blocks.OBSIDIAN);
else for(int y=FLOOR+1;y<=FLOOR+32;y++)put(m,x,y,z,Blocks.AIR);
}
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)put(m,x,FLOOR,z,Blocks.CALCITE);
// The stem stops below the chart. Keep the centre of the entire room and portal hollow.
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)
for(int h=1;h<=8;h++)put(m,x,FLOOR+h,z,Blocks.AIR);
return Collections.unmodifiableMap(m);
}
static Map<String,Object> check(ServerLevel level,Map<BlockPos,BlockState> plan){
if(level.getHeight()!=1600)throw new IllegalStateException("Fine stem needs the 1600-high laboratory");
for(var e:plan.entrySet())if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("Fine stem/palace mismatch "+e.getKey());
int thinSections=0;
// Verify actual empty space around every thin section, including the removed helix and leaves.
for(int y=652;y<CROWN_BOTTOM;y++){
int solid=0;
for(int x=-18;x<=18;x++)for(int z=-18;z<=18;z++)if(!level.getBlockState(new BlockPos(x,y,z)).isAir())solid++;
if(solid!=1||!level.getBlockState(new BlockPos(0,y,0)).is(Blocks.CALCITE))throw new IllegalStateException("Stem wider than one block at "+y+": "+solid);
thinSections++;
}
if(plan.values().stream().anyMatch(b->b.is(Blocks.TUFF_BRICK_STAIRS)))throw new IllegalStateException("Stair retained in new palace");
var old=Palace184.plan(level.getSeed(),Station183.layout(level.getChunkSource().getGenerator())).blocks();
var approved=new HashMap<BlockPos,BlockState>();var current=new HashMap<BlockPos,BlockState>();
old.forEach((p,b)->{if(p.getY()>=FLOOR+4&&(Math.abs(p.getX())>3||Math.abs(p.getZ())>3))approved.put(p,b);});
plan.forEach((p,b)->{if(p.getY()>=FLOOR+4&&(Math.abs(p.getX())>3||Math.abs(p.getZ())>3))current.put(p,b);});
if(!approved.equals(current))throw new IllegalStateException("Upper palace changed outside portal recess");
int portalRim=0,supportedWindows=0;
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++){
var p=new BlockPos(x,PORTAL_Y,z);
if(Math.abs(x)==3||Math.abs(z)==3){
var b=level.getBlockState(p);
if(!b.is(Blocks.OBSIDIAN)&&!b.is(Blocks.CRYING_OBSIDIAN))throw new IllegalStateException("Missing roof portal rim");
boolean attached=false;
for(var d:Direction.Plane.HORIZONTAL){var neighbour=p.relative(d);if((Math.abs(neighbour.getX())>3||Math.abs(neighbour.getZ())>3)&&!level.getBlockState(neighbour).isAir())attached=true;}
if(!attached)throw new IllegalStateException("Roof portal disconnected from dome");portalRim++;
}else for(int y=FLOOR+1;y<=FLOOR+32;y++)if(!level.getBlockState(new BlockPos(x,y,z)).isAir())throw new IllegalStateException("Blocked roof portal aperture");
}
for(var e:plan.entrySet()){
var p=e.getKey();if(p.getY()!=FLOOR+2)continue;
for(var glass:Rosette183.GLASS)if(e.getValue().is(glass)){
if(!level.getBlockState(p.below()).is(Blocks.CALCITE)||level.getBlockState(p.below(2)).isAir())throw new IllegalStateException("Gap under palace glass "+p);
supportedWindows++;
}
}
if(supportedWindows<50||portalRim!=24)throw new IllegalStateException("Roof portal or glazing coverage");
for(var e:Rosette183.plan(level.getSeed()).entrySet())if(!level.getBlockState(e.getKey().above(320)).equals(e.getValue()))throw new IllegalStateException("Approved origin rosette changed");
if(!level.getBlockState(new BlockPos(0,ROOT,0)).is(OriginBlock.BLOCK))throw new IllegalStateException("Origin moved");
int boatApproaches=0;
int[][] directions={{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
// Native collision shapes, a standard boat's hull plus pilot, flying above moss carpets.
for(int[] direction:directions)for(int feet:new int[]{ROOT-1,FLOOR+1}){
int end=feet==ROOT-1?4:7,start=feet==ROOT-1?12:34;
for(int distance=end;distance<=start;distance++){
double x=direction[0]*distance+.5,z=direction[1]*distance+.5;
if(!level.noCollision(new AABB(x-.6875,feet+.125,z-.6875,x+.6875,feet+2.5,z+.6875)))throw new IllegalStateException("Flying boat entrance blocked at "+x+", "+feet+", "+z);
}
boatApproaches++;
}
var result=new LinkedHashMap<String,Object>();
result.put("origin","0, 640, 0");result.put("thinCrossSections",thinSections);result.put("stemWidth",1);
result.put("stairTreads",0);result.put("landings",0);result.put("palaceFloor",FLOOR);result.put("portalY",PORTAL_Y);
result.put("preservedUpperShellOutsidePortal",true);result.put("supportedWindowColumns",supportedWindows);
result.put("clearBoatApproaches",boatApproaches);result.put("blocks",plan.size());return result;
}
}
@@ -0,0 +1,65 @@
package fr.koka.sanctuarytest;
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.block.state.BlockState;
import net.minecraft.world.phys.AABB;
/** Fine inlaid bands on the approved silhouette, with completely open balcony edges. */
final class Palace186 {
private Palace186() {}
private static boolean glass(BlockState state) {
for(var block:Rosette183.GLASS)if(state.is(block))return true;
return state.is(Blocks.STAINED_GLASS.white());
}
static Map<BlockPos,BlockState> plan(long seed,Station183.Layout layout) {
var result=new LinkedHashMap<>(Palace185.plan(seed,layout));
result.replaceAll((p,state)->{
if(state.is(Blocks.MOSSY_COBBLESTONE_WALL)&&p.getY()>Palace185.FLOOR)return Blocks.AIR.defaultBlockState();
if(!glass(state))return state;
int y=p.getY(),sector=Rosette183.sector(p.getX(),p.getZ());
boolean band=y==643||y==647||y==Palace185.FLOOR+8||y==Palace185.FLOOR+20;
if(band)return ((sector&1)==0?Blocks.CALCITE:Blocks.CUT_COPPER.waxed().oxidized()).defaultBlockState();
// Eight small diamonds interrupt the large panels; they stay entirely inside the shell.
int h=y-Palace185.FLOOR;
if(h>=13&&h<=15){
double angle=sector*Math.PI/4,side=Math.abs(p.getX()*Math.cos(angle)+p.getZ()*Math.sin(angle));
if(side<=1.1-Math.abs(h-14)*.8)
return (h==14&&side<.4?Blocks.CUT_COPPER.waxed().oxidized():Blocks.CALCITE).defaultBlockState();
}
return state;
});
return Collections.unmodifiableMap(result);
}
static Map<String,Object> check(ServerLevel level,Map<BlockPos,BlockState> plan) {
var old=Palace185.plan(level.getSeed(),Station183.layout(level.getChunkSource().getGenerator()));
if(!old.keySet().equals(plan.keySet()))throw new IllegalStateException("Ornament changed palace outline");
int rails=0,inlays=0,remainingGlass=0,originalGlass=0;int[][] colors=new int[2][8];
for(var e:plan.entrySet()){
var p=e.getKey();var state=e.getValue();var before=old.get(p);
if(!level.getBlockState(p).equals(state))throw new IllegalStateException("Ornament mismatch at "+p);
if(glass(before))originalGlass++;
if(!before.equals(state)){
if(before.is(Blocks.MOSSY_COBBLESTONE_WALL)&&state.isAir()&&p.getY()>Palace185.FLOOR)rails++;
else if(glass(before)&&(state.is(Blocks.CALCITE)||state.is(Blocks.CUT_COPPER.waxed().oxidized())))inlays++;
else throw new IllegalStateException("Unapproved palace alteration "+p);
}
if(state.is(Blocks.MOSSY_COBBLESTONE_WALL))throw new IllegalStateException("Upper railing remains");
if(glass(state)){
remainingGlass++;
for(int i=0;i<8;i++)if(state.is(Rosette183.GLASS[i]))colors[p.getY()<700?0:1][i]++;
}
}
if(rails==0||inlays<60||remainingGlass<originalGlass*.78)throw new IllegalStateException("Ornament coverage");
for(var ring:colors)for(int count:ring)if(count<8)throw new IllegalStateException("A compass colour was lost");
for(int[] d:new int[][]{{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}})
for(int feet:new int[]{639,1279})for(int distance=feet==639?4:7;distance<=(feet==639?12:34);distance++){
double x=d[0]*distance+.5,z=d[1]*distance+.5;
if(!level.noCollision(new AABB(x-.6875,feet+.125,z-.6875,x+.6875,feet+2.5,z+.6875)))throw new IllegalStateException("Ornament blocks boat approach");
}
return Map.of("removedRailings",rails,"inlaidBlocks",inlays,"retainedGlass",remainingGlass,
"sameOutline",true,"colorsByRosette",colors,"clearBoatApproaches",16,"portalY",Palace185.PORTAL_Y);
}
}
@@ -0,0 +1,115 @@
package fr.koka.sanctuarytest;
import java.util.*;
import java.nio.charset.StandardCharsets;
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.decoration.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.saveddata.maps.*;
/** Native saved maps in native horizontal frames. Bounded preview jobs never request remote chunks. */
final class PalaceAtlas184 {
private static final String TAG="sanctuary_test:palace_atlas184",PENDING=TAG+"_pending";
private static final Map<ServerLevel,LinkedHashMap<UUID,Job>> JOBS=new WeakHashMap<>();
private static final class Job {
final ItemFrame frame;final MapId id;final MapItemSavedData data;int row;
Job(ItemFrame frame,MapId id,MapItemSavedData data){this.frame=frame;this.id=id;this.data=data;}
}
private PalaceAtlas184() {}
static void register(){
ServerEntityEvents.ENTITY_LOAD.register((entity,level)->{if(entity instanceof ItemFrame frame&&frame.entityTags().contains(PENDING)&&(WorldgenLab.ascent184(level.getChunkSource().getGenerator())||WorldgenLab.stem185(level.getChunkSource().getGenerator())||WorldgenLab.anchors186(level.getChunkSource().getGenerator())||WorldgenLab.natural187(level.getChunkSource().getGenerator())||WorldgenLab.details188(level.getChunkSource().getGenerator())||WorldgenLab.finishedTerrain(level.getChunkSource().getGenerator())))enqueue(level,frame);});
ServerTickEvents.END_SERVER_TICK.register(server->{var level=server.overworld();long until=System.nanoTime()+3_000_000;while(hasWork(level)&&System.nanoTime()<until)step(level);});
ServerLifecycleEvents.SERVER_STOPPED.register(server->JOBS.remove(server.overworld()));
}
static void populate(WorldGenLevel level,ChunkGenerator generator,ChunkAccess c){
var layout=Station183.layout(generator);
if(c.getPos().x()< -1||c.getPos().x()>0||c.getPos().z()< -1||c.getPos().z()>0)return;
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++){
var at=new BlockPos(x,Palace184.FLOOR+1,z);if(!c.getPos().contains(at))continue;
var frame=new GlowItemFrame(level.getLevel(),at,Direction.UP);
frame.setUUID(UUID.nameUUIDFromBytes((TAG+":"+level.getSeed()+":"+x+":"+z).getBytes(StandardCharsets.UTF_8)));
frame.addTag(TAG);frame.addTag(PENDING);level.addFreshEntity(frame);
}
}
private static void enqueue(ServerLevel level,ItemFrame frame){
var queue=JOBS.computeIfAbsent(level,l->new LinkedHashMap<>());if(queue.containsKey(frame.getUUID()))return;
var p=frame.blockPosition();var stack=frame.getItem();
if(!stack.is(Items.FILLED_MAP)){
stack=MapItem.create(level,p.getX()*128,p.getZ()*128,(byte)0,false,false);
stack.set(DataComponents.ITEM_NAME,Component.translatable("item.sanctuary_test.iss_chart",p.getX(),p.getZ()));frame.setItem(stack,false);frame.setRotation(0);
}
var id=stack.get(DataComponents.MAP_ID);var data=MapItem.getSavedData(stack,level);
if(data==null)throw new IllegalStateException("Missing ISS native map data");
queue.put(frame.getUUID(),new Job(frame,id,data));
}
private static boolean hasWork(ServerLevel l){var q=JOBS.get(l);return q!=null&&!q.isEmpty();}
private static void step(ServerLevel level){
var q=JOBS.get(level);var entry=q.entrySet().iterator().next();var job=entry.getValue();
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();
// Four map rows per slice, terrain sampled at quart resolution and expanded in the native raster.
for(int x=0;x<128;x+=4){
int wx=job.data.centerX-64+x,wz=job.data.centerZ-64+job.row;
byte color=color(level,source,wx,wz);
for(int dx=0;dx<4;dx++)for(int dz=0;dz<4;dz++)job.data.setColor(x+dx,job.row+dz,color);
}
job.row+=4;
if(job.row==128){level.setMapData(job.id,job.data.locked());job.frame.removeTag(PENDING);q.remove(entry.getKey());}
}
private static byte color(ServerLevel level,AdventureBiomes180 s,int x,int z){
int y=s.field().surface(x,z);
if(y<0)return MapColor.COLOR_BLACK.getPackedId(MapColor.Brightness.LOW);
MapColor color;
// Already loaded chunks provide their real ground palette; no new chunk is ever requested here.
var chunk=level.getChunkSource().getChunkNow(x>>4,z>>4);
if(chunk!=null){
int top=Math.min(316,chunk.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.WORLD_SURFACE,x&15,z&15)+1);
BlockPos p=new BlockPos(x,top,z);
while(p.getY()>0&&(chunk.getBlockState(p).isAir()||chunk.getBlockState(p).getMapColor(level,p)==MapColor.NONE))p=p.below();
color=chunk.getBlockState(p).getMapColor(level,p);
if(color==MapColor.NONE)color=MapColor.COLOR_BLACK;
}else{
String biome=s.resolve(x,y,z).unwrapKey().orElseThrow().identifier().getPath();
color=switch(biome){case "adventure_autumn"->MapColor.COLOR_ORANGE;case "adventure_cliffs"->MapColor.STONE;case "adventure_forest"->MapColor.PLANT;default->MapColor.GRASS;};
}
int north=s.field().surface(x,z-4);
var shade=north<0||y==north?MapColor.Brightness.NORMAL:y>north?MapColor.Brightness.HIGH:MapColor.Brightness.LOW;
// Surface clue remains visible even in the inexpensive projected part of the chart.
if(!s.naturalSulfur()){var sulfur=Sulfur183.site(s);if(Math.hypot(x-sulfur.x(),z-sulfur.z())<12)color=MapColor.COLOR_YELLOW;}
else if(SurfaceWater187.surfaceAt(s,x,z)>=y)color=MapColor.WATER;
return color.getPackedId(shade);
}
static Map<String,Object> check(ServerLevel level){
var layout=Station183.layout(level.getChunkSource().getGenerator());
var manager=((fr.koka.sanctuary.mixin.ExpansionVoidServerLevelAccess)level).sanctuary$entityManager();
var chunks=new ArrayList<net.minecraft.world.level.ChunkPos>();
for(int x=-1;x<=0;x++)for(int z=-1;z<=0;z++){
var cp=new net.minecraft.world.level.ChunkPos(x,z);chunks.add(cp);level.getChunk(x,z);
manager.updateChunkStatus(cp,net.minecraft.world.level.entity.Visibility.TRACKED);
}
long deadline=System.nanoTime()+5_000_000_000L;
level.getServer().managedBlock(()->{manager.processPendingLoads();return chunks.stream().allMatch(cp->manager.areEntitiesLoaded(cp.pack()))||System.nanoTime()>deadline;});
if(chunks.stream().anyMatch(cp->!manager.areEntitiesLoaded(cp.pack())))throw new IllegalStateException("ISS entity loading timed out");
var frames=new ArrayList<ItemFrame>();
for(var entity:level.getAllEntities())if(entity instanceof ItemFrame f&&f.entityTags().contains(TAG)){frames.add(f);if(f.entityTags().contains(PENDING))enqueue(level,f);}
if(frames.size()!=layout.tiles()*layout.tiles())throw new IllegalStateException("Missing atlas frames: "+frames.size());
long started=System.nanoTime();while(hasWork(level))step(level);
var ids=new HashSet<MapId>();int painted=0;
for(var frame:frames){
if(frame.getDirection()!=Direction.UP||!frame.survives())throw new IllegalStateException("Unsupported horizontal frame "+frame.blockPosition());
var id=frame.getItem().get(DataComponents.MAP_ID);var data=level.getMapData(id);var p=frame.blockPosition();
if(!ids.add(id)||data==null||!data.locked||data.centerX!=p.getX()*128||data.centerZ!=p.getZ()*128)throw new IllegalStateException("Atlas tile continuity/persistence mismatch");
for(byte b:data.colors)if(b!=0)painted++;
}
if(painted!=frames.size()*128*128)throw new IllegalStateException("Blank chart pixels");
return Map.of("frames",frames.size(),"tilesPerSide",layout.tiles(),"mapScale",0,"coveredDiameter",layout.tiles()*128,"previewMs",(System.nanoTime()-started)/1_000_000,"remoteChunkLoads",0,"tableCenter","0, 1279, 0");
}
}
@@ -0,0 +1,91 @@
package fr.koka.sanctuarytest;
import com.google.gson.*;
import fr.koka.sanctuary.SanctuaryMod;
import java.nio.file.*;
import java.util.*;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.storage.LevelResource;
/** Opt-in native Vulkan screenshots in disposable photo worlds only; inactive during ordinary visits. */
final class PhotoVisit193 {
record Shot(String name,double x,double y,double z,float yaw,float pitch){}
private static List<Shot> shots;
private static int index=-1,ticks,settled;
private static boolean writing,finished,confirmed,identitySubmitted;
private static long arrived;
static void register(){
var path=System.getProperty("sanctuary.photo193");if(path==null)return;
try{
var values=JsonParser.parseString(Files.readString(Path.of(path))).getAsJsonArray();var list=new ArrayList<Shot>();
for(var v:values){var o=v.getAsJsonObject();var name=o.get("name").getAsString();
if(!name.matches("[a-z0-9_-]{1,64}"))throw new IllegalArgumentException("Invalid photograph name");
list.add(new Shot(name,o.get("x").getAsDouble(),o.get("y").getAsDouble(),o.get("z").getAsDouble(),o.get("yaw").getAsFloat(),o.get("pitch").getAsFloat()));
}
var run=Path.of(path).toAbsolutePath().normalize().getParent();
var world=JsonParser.parseString(Files.readString(run.resolve("solo.json"))).getAsJsonObject().get("world").getAsString();
if(!world.matches("Sanctuary-Photo-193-[0-9-]+"))throw new IllegalArgumentException("Wrong disposable photo world");
net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.AFTER_INIT.register((mc,screen,w,h)->{
SanctuaryMod.LOGGER.info("PHOTO193_SCREEN {} {}",screen.getClass().getName(),screen.getTitle().getString());
if(!mc.gameDirectory.toPath().toAbsolutePath().normalize().equals(run.resolve("client")))throw new IllegalStateException("Photo client directory differs from its manifest");
if(!identitySubmitted&&screen instanceof fr.koka.sanctuary.client.HelloWorldScreen){
identitySubmitted=true;mc.execute(()->{
if(mc.gui.screen()!=screen)return;
var input=new net.minecraft.client.input.KeyEvent(257,0,0);
for(var suffix:List.of("$ColorButton","$StarterSlot"))for(var child:screen.children())
if(child instanceof net.minecraft.client.gui.components.Button b&&child.getClass().getName().endsWith(suffix)&&b.active){b.onPress(input);break;}
for(var child:screen.children())if(child instanceof net.minecraft.client.gui.components.EditBox box
&&box.getMessage().getString().equals(net.minecraft.network.chat.Component.translatable("sanctuary.starter.name").getString()))box.setValue("Labo");
for(var child:screen.children())if(child instanceof net.minecraft.client.gui.components.Button b&&b.active
&&b.getMessage().getString().equals(fr.koka.sanctuary.progression.ProgressionService.label("enter").getString())){b.onPress(input);break;}
});
}
// This warning is expected for our newly copied experimental preset. Never accept a general dialog.
if(!confirmed&&screen instanceof net.minecraft.client.gui.screens.BackupConfirmScreen
&&screen.getTitle().getString().equals(net.minecraft.network.chat.Component.translatable("selectWorld.backupQuestion.experimental").getString())){
for(var child:screen.children())if(child instanceof net.minecraft.client.gui.components.Button b
&&b.getMessage().getString().equals(net.minecraft.network.chat.Component.translatable("selectWorld.backupJoinSkipButton").getString())){
confirmed=true;mc.execute(()->{if(mc.gui.screen()==screen)b.onPress(new net.minecraft.client.input.KeyEvent(257,0,0));});break;
}
}
});
shots=List.copyOf(list);if(shots.isEmpty())throw new IllegalArgumentException("Empty visit");
}catch(Exception e){throw new IllegalStateException("Invalid photo visit",e);}
ClientTickEvents.END_CLIENT_TICK.register(PhotoVisit193::tick);
}
private static void tick(Minecraft mc){
if(finished||mc.level==null||mc.player==null||mc.getSingleplayerServer()==null)return;
var server=mc.getSingleplayerServer();
if(!server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().getFileName().toString().startsWith("Sanctuary-Photo-193-"))throw new IllegalStateException("Photographs require a disposable photo world");
if(!WorldgenLab.coherent193(server.overworld().getChunkSource().getGenerator()))throw new IllegalStateException("Wrong photograph generator");
if(!mc.gui.hud.isHidden())mc.gui.hud.toggle();mc.gui.setScreen(null);
if(writing)return;
if(index<0||ticks<0){
index++;ticks=0;settled=0;arrived=System.nanoTime();
if(index==shots.size()){
finished=true;SanctuaryMod.LOGGER.info("PHOTO193_FINISHED shots={}",index);mc.stop();return;
}
var shot=shots.get(index);var id=mc.player.getUUID();
server.execute(()->{var player=server.getPlayerList().getPlayer(id);if(player!=null){player.setGameMode(GameType.SPECTATOR);player.connection.teleport(shot.x,shot.y,shot.z,shot.yaw,shot.pitch);}});
SanctuaryMod.LOGGER.info("PHOTO193_CAMERA {} {} {} {} yaw={} pitch={}",shot.name,shot.x,shot.y,shot.z,shot.yaw,shot.pitch);
return;
}
ticks++;
var shot=shots.get(index);
if(mc.player.distanceToSqr(shot.x,shot.y,shot.z)>4)return;
if(mc.levelRenderer.hasRenderedAllSections())settled++;else settled=0;
double seconds=(System.nanoTime()-arrived)/1e9;
if(seconds>180){finished=true;SanctuaryMod.LOGGER.error("PHOTO193_FAILED render timeout {}",shot.name);mc.stop();return;}
mc.player.setYRot(shot.yaw);mc.player.setXRot(shot.pitch);
if(seconds<12||settled<20)return;
writing=true;
Screenshot.grab(mc.gameDirectory,shot.name+".png",mc.gameRenderer.mainRenderTarget(),1,message->mc.execute(()->{
var file=mc.gameDirectory.toPath().resolve("screenshots").resolve(shot.name+".png");
if(!Files.isRegularFile(file))throw new IllegalStateException("Screenshot failed: "+message.getString());
SanctuaryMod.LOGGER.info("PHOTO193_SAVED {} renderSettledTicks={}",file,settled);writing=false;ticks=-1;
}));
}
}
@@ -11,6 +11,10 @@ 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();
ClientConfigurationNetworking.registerGlobalReceiver(QuickTestMod.Mode.TYPE,(payload,context)->
context.client().execute(()->IntroClient.setQuickTestIntro(payload.quick())));
ScreenEvents.AFTER_INIT.register((minecraft,screen,width,height)->{
@@ -19,6 +19,10 @@ public final class QuickTestMod implements ModInitializer {
DuoLab.register();
HordeLab.register();
PalaceLab.register();
AnchorOffers186.register();
AnchorOffers187.register();
AnchorOffers188.register();
AnchorOffers189.register();
WorldgenLab.register();
PayloadTypeRegistry.clientboundConfiguration().register(Mode.TYPE,Mode.CODEC);
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server)->{
@@ -0,0 +1,104 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.expansion.*;
import fr.koka.sanctuary.palace.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BooleanSupplier;
import net.fabricmc.fabric.api.event.player.*;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLevelEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.*;
import net.minecraft.world.*;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
/** New-world relay offers. The durable reservation precedes charging the held stack. */
final class RelayOffers199 {
private record Key(ServerLevel level,BlockPos pos) {}
private record Offer(String id,String parent,String climate,int sector,boolean root) {}
private static final Set<Key> PENDING=new HashSet<>();
private RelayOffers199() {}
static void register(){
ServerLevelEvents.UNLOAD.register((server,level)->PENDING.removeIf(k->k.level()==level));
PlayerBlockBreakEvents.BEFORE.register((world,player,pos,state,entity)->!(world instanceof ServerLevel level)||!reserved(level,pos));
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
if(!(world instanceof ServerLevel level)||!(player instanceof ServerPlayer p)||p.isSpectator()
||hand!=InteractionHand.MAIN_HAND||LabExpansions199.get(level.getChunkSource().getGenerator())==null)return InteractionResult.PASS;
var pos=hit.getBlockPos();
if(reserved(level,pos)||reserved(level,pos.relative(hit.getDirection())))return InteractionResult.FAIL;
if(!level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK))return InteractionResult.PASS;
var held=p.getItemInHand(hand);
var result=offer(level,pos,held,()->!p.hasDisconnected()&&p.level()==level
&&p.blockPosition().distSqr(pos)<64&&p.getItemInHand(hand)==held);
if(!result.isDone())p.sendSystemMessage(Component.translatable("sanctuary_test.expansion199.searching"),true);
result.whenComplete((island,error)->{
if(p.hasDisconnected())return;
if(error==null)p.sendSystemMessage(Component.translatable("sanctuary_test.expansion199."+(island.status().equals("ready")?"already":"preparing")),true);
else {
var cause=error;while(cause.getCause()!=null)cause=cause.getCause();
String key=Set.of("required","core_off","invalid","already_searching","expansion_busy","offer_changed","no_free_space","island_limit","world_closed").contains(cause.getMessage())?cause.getMessage():"failed";
var demand=AnchorOffers189.DEMANDS.get(level.getBlockState(pos).is(ExpansionAnchorBlock.BLOCK)?level.getBlockState(pos).getValue(ExpansionAnchorBlock.SECTOR):0);
p.sendSystemMessage(Component.translatable("sanctuary_test.expansion199."+key,demand.count(),new ItemStack(demand.item()).getHoverName()),true);
if(key.equals("failed"))fr.koka.sanctuary.SanctuaryMod.LOGGER.warn("Expansion offer failed at {}",pos,cause);
}
});
return InteractionResult.SUCCESS;
});
}
static boolean reserved(ServerLevel level,BlockPos pos){
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
var region=adapter==null?null:adapter.regionAt(pos.getX(),pos.getZ());
return region!=null&&!region.status().equals("ready");
}
private static Offer describe(ServerLevel level,BlockPos pos){
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
var block=level.getBlockState(pos);
if(adapter==null||!block.is(ExpansionAnchorBlock.BLOCK))throw new IllegalArgumentException("invalid");
int sector=block.getValue(ExpansionAnchorBlock.SECTOR);
var parent=adapter.regionAt(pos.getX(),pos.getZ());
boolean root=parent==null;
if(root){
var site=Anchors189.plan(Anchors189.source(level.getChunkSource().getGenerator()),level.getSeed()).sites().stream().filter(s->s.anchor().equals(pos)&&s.sector()==sector).findFirst();
if(site.isEmpty())throw new IllegalArgumentException("invalid");
}else if(!parent.status().equals("ready")||!adapter.relay(parent).equals(pos))throw new IllegalArgumentException("invalid");
String parentId=root?"sanctuary":parent.id();
String id="r"+Long.toUnsignedString(ExpansionIsland.deriveSeed(level.getSeed(),parentId+":"+pos.asLong()),36);
return new Offer(id,parentId,root?ExpansionIsland.directionalClimate(ExpansionIsland.DIRECTIONS.get(sector)):parent.climate(),sector,root);
}
static CompletableFuture<ExpansionIsland> offer(ServerLevel level,BlockPos pos,ItemStack held,BooleanSupplier playerValid){
try {
var offer=describe(level,pos);var session=ExpansionRuntime.session(level);
var existing=session.islands().stream().filter(i->i.id().equals(offer.id())).findFirst();
if(existing.isPresent())return CompletableFuture.completedFuture(existing.get());
var key=new Key(level,pos.immutable());
if(PENDING.contains(key))throw new IllegalArgumentException("already_searching");
var core=level.getBlockState(AnchorOffers189.CORE);
if(offer.root()&&(!core.is(OriginBlock.BLOCK)||!core.getValue(OriginBlock.LIT)))throw new IllegalArgumentException("core_off");
var demand=AnchorOffers189.DEMANDS.get(offer.sector());
if(!held.is(demand.item())||held.getCount()<demand.count())throw new IllegalArgumentException("required");
BooleanSupplier valid=()->{
if(!playerValid.getAsBoolean()||!held.is(demand.item())||held.getCount()<demand.count())return false;
try{var current=level.getBlockState(AnchorOffers189.CORE);return describe(level,pos).equals(offer)&&(!offer.root()||current.is(OriginBlock.BLOCK)&&current.getValue(OriginBlock.LIT));}catch(IllegalArgumentException e){return false;}
};
var adapter=LabExpansions199.get(level.getChunkSource().getGenerator());
boolean north=adapter.rootGeneration()>=200&&offer.sector()==0;
var request=adapter.rootGeneration()>=200
?session.requestNearby(offer.id(),offer.parent(),north?"glacial":offer.climate(),north?(adapter.rootGeneration()>=201?1024:2048):256,ExpansionIsland.DIRECTIONS.get(offer.sector()),pos.getX(),pos.getZ(),valid)
:session.requestNearby(offer.id(),offer.parent(),offer.climate(),256,valid);
PENDING.add(key);
return request.whenComplete((island,error)->{
PENDING.remove(key);
if(error!=null)return;
held.shrink(demand.count());
var anchor=level.getBlockState(pos);
level.setBlock(pos,anchor.setValue(ExpansionAnchorBlock.LIT,true),Block.UPDATE_ALL);
if(offer.root()){
var state=level.getBlockState(AnchorOffers189.CORE);
if(state.is(OriginBlock.BLOCK))level.setBlock(AnchorOffers189.CORE,OriginBlock.relay(state,offer.sector(),true),Block.UPDATE_ALL);
}
});
}catch(Exception e){return CompletableFuture.failedFuture(e);}
}
}
@@ -8,9 +8,13 @@ import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** A local wooded summit; the deep island and the aerial field remain unchanged. */
public record ReliefEcology177(DensityFunction base) implements DensityFunction {
public record ReliefEcology177(DensityFunction base, boolean terraces, boolean refined, boolean eroded, boolean coherent) implements DensityFunction {
public static final MapCodec<ReliefEcology177> CODEC=RecordCodecBuilder.mapCodec(i->i.group(
DensityFunction.CODEC.fieldOf("base").forGetter(ReliefEcology177::base)
DensityFunction.CODEC.fieldOf("base").forGetter(ReliefEcology177::base),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("terraces",false).forGetter(ReliefEcology177::terraces),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("refined",false).forGetter(ReliefEcology177::refined),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("eroded",false).forGetter(ReliefEcology177::eroded),
com.mojang.serialization.Codec.BOOL.optionalFieldOf("coherent",false).forGetter(ReliefEcology177::coherent)
).apply(i,ReliefEcology177::new));
@Override public DensitySampler compileSampler(CompileContext context){
var baseField=(SkyFragments175.Field)DfRewriteRule.INLINE_REFERENCE.rewrite(base).compileSampler(context);
@@ -22,7 +26,7 @@ public record ReliefEcology177(DensityFunction base) implements DensityFunction
int y=top(baseField,x,z);
if(y>bestY){bestX=x;bestZ=z;bestY=y;}
}
return new Field(baseField,bestX,bestZ);
return new Field(baseField,bestX,bestZ,terraces,refined,eroded,coherent);
}
private static int top(DensitySampler field,int x,int z){
for(int y=316;y>=48;y-=4)if(field.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0){
@@ -33,19 +37,58 @@ public record ReliefEcology177(DensityFunction base) implements DensityFunction
public static final class Field implements DensitySampler {
private final SkyFragments175.Field base;
private final int peakX,peakZ;
private final boolean terraces,refined;
private final Erosion192 erosion;
// Quart columns are shared by all biome heights. Fixed island bounds limit this cache.
private final ConcurrentHashMap<Long,Integer> tops=new ConcurrentHashMap<>();
Field(SkyFragments175.Field base,int x,int z){this.base=base;peakX=x;peakZ=z;}
Field(SkyFragments175.Field base,int x,int z,boolean terraces){this(base,x,z,terraces,false);}
Field(SkyFragments175.Field base,int x,int z,boolean terraces,boolean refined){this(base,x,z,terraces,refined,false,false);}
Field(SkyFragments175.Field base,int x,int z,boolean terraces,boolean refined,boolean eroded,boolean coherent){
this.base=base;peakX=x;peakZ=z;this.terraces=terraces;this.refined=refined;
erosion=eroded?new Erosion192(new Field(base,x,z,false,false),coherent):null;
}
public boolean coherent(){return erosion!=null&&erosion.coherent();}
public boolean eroded(){return erosion!=null;}
Erosion192 erosion(){return erosion;}
public boolean refined(){return refined;}
public boolean terraces(){return terraces;}
public SkyFragments175.Field base(){return base;}
public int peakX(){return peakX;}public int peakZ(){return peakZ;}
public int cachedColumns(){return tops.size();}
public int surface(int x,int z){
x=x>>2<<2;z=z>>2<<2;
if(Math.hypot(x,z)>398)return -1;
if(Math.hypot(x,z)>base.terrainBound())return -1;
final int sx=x,sz=z;
return tops.computeIfAbsent(((long)x<<32)^(z&0xffffffffL),ignored->top(this,sx,sz));
}
@Override public float sampleValue(SamplerContext context,int x,int y,int z){
if(terraces && y>252 && y<316){
// Analytic vertical remap: four broad benches, no new noise or terrain scan.
double t=(y-252)/16.0;int cell=(int)t;double f=t-cell;
double q=f<.25?f*.20:f>.75?1-(1-f)*.20:.05+(f-.25)*1.8;
double at=252+(cell+q)*16;int lo=(int)Math.floor(at);
// Conservatively retain thin overhangs when a terrace compresses two source voxels.
return Math.max(raw(context,x,lo,z),raw(context,x,lo+1,z));
}
if(refined && y>256 && y<312){
long salt=((long)peakX<<32)^peakZ^0x190L;
// Sparse, smoothly bounded shoulders. No repeated horizontal levels around the island.
double patch=EcologyBiomes176.noise(salt,x/65.0,z/65.0);
if(patch>.28){
double weight=Math.min(1,(patch-.28)/.32);weight=weight*weight*(3-2*weight);
double center=282+9*EcologyBiomes176.noise(salt^91,x/43.0,z/43.0);
double dy=(y-center)/12;
if(Math.abs(dy)<1){
double at=y+2.2*weight*Math.sin(Math.PI*dy);int lo=(int)Math.floor(at);
// Keep thin natural ledges: a local shoulder may widen them, never erase them.
return Math.max(raw(context,x,y,z),(float)(raw(context,x,lo,z)*(1-(at-lo))+raw(context,x,lo+1,z)*(at-lo)));
}
}
}
float value=raw(context,x,y,z);
return erosion!=null && y>240 && y<320?Math.min(value,(float)((erosion.roof(x,z)-y)*.15)):value;
}
private float raw(SamplerContext context,int x,int y,int z){
if(y<=216 || y>=320)return base.sampleValue(context,x,y,z);
double radius=((x-peakX)*(double)(x-peakX)+(z-peakZ)*(double)(z-peakZ))/(96*96.0);
if(radius>=1)return base.sampleValue(context,x,y,z);
@@ -57,7 +100,7 @@ public record ReliefEcology177(DensityFunction base) implements DensityFunction
}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
}
@Override public DensityFunction rewriteChildren(DfRewriteRule rule){return new ReliefEcology177(rule.rewrite(base));}
@Override public DensityFunction rewriteChildren(DfRewriteRule rule){return new ReliefEcology177(rule.rewrite(base),terraces,refined,eroded,coherent);}
@Override public Interval range(){return Interval.of(-1,1);}
@Override public int domainAxes(){return ALL_AXES;}
@Override public MapCodec<ReliefEcology177> codec(){return CODEC;}
@@ -0,0 +1,70 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.palace.OriginBlock;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
/** Eight intercardinal ribs surround eight open views, on the retained natural palette. */
public final class Rosette182 {
private static final int[][] AXES={{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1},{0,-1},{1,-1}};
private Rosette182() {}
private static boolean axis(int x,int z){return Math.min(Math.min(Math.abs(x),Math.abs(z)),Math.abs(Math.abs(x)-Math.abs(z)))<=1;}
private static void set(Map<BlockPos,BlockState> map,int x,int y,int z,Block block){map.put(new BlockPos(x,y,z),block.defaultBlockState());}
static Map<BlockPos,BlockState> plan(long seed) {
var map=new LinkedHashMap<BlockPos,BlockState>();
Origin181.plan(seed).forEach((p,b)->{
if(p.getY()<=318||p.equals(Origin181.CORE)||Math.abs(p.getX())<=1&&Math.abs(p.getZ())<=1||b.is(Blocks.MOSS_CARPET))map.put(p,b);
});
// Thin continuous ledge supports the glass and the inter-axis ribs without widening the landmark.
for(int x=-9;x<=9;x++)for(int z=-9;z<=9;z++) {
double r=Math.hypot(x,z);if(r>9)continue;
if(!map.containsKey(new BlockPos(x,318,z)))for(int y=316;y<=318;y++)set(map,x,y,z,Blocks.TUFF);
if(r>=7.5&&r<=8.5&&!axis(x,z))for(int y=319;y<=323;y++)set(map,x,y,z,Blocks.GLASS);
// Connected voxel shell of a low half-ellipsoid, with empty interior.
for(int h=0;h<=8;h++) {
double here=(x*x+z*z)/67.24+(h*h)/64.0;
if(here>1)continue;
double above=(x*x+z*z)/67.24+((h+1)*(h+1))/64.0;
double outward=((Math.abs(x)+1)*(Math.abs(x)+1)+(Math.abs(z)+1)*(Math.abs(z)+1))/67.24+(h*h)/64.0;
if(above>1||outward>1)set(map,x,323+h,z,Blocks.STAINED_GLASS.white());
}
}
for(int sector=0;sector<8;sector++) {
double a=(sector+.5)*Math.PI/4;
for(int h=0;h<=12;h++) {
double r=h<=4?8:8*Math.sqrt(Math.max(0,1-(h-4)*(h-4)/64.0));
int x=(int)Math.round(Math.cos(a)*r),z=(int)Math.round(Math.sin(a)*r);
set(map,x,319+h,z,h<3?Blocks.CALCITE:h==3?Blocks.CHISELED_TUFF:Blocks.CUT_COPPER.waxed().oxidized());
}
}
// No decorative support may occupy an approach at player height.
for(var d:AXES)for(int step=2;step<=8;step++) {
int x=d[0]*step,z=d[1]*step;if(Math.hypot(x,z)>9)continue;
for(int y=319;y<=321;y++)map.remove(new BlockPos(x,y,z));
}
return Collections.unmodifiableMap(map);
}
public static void place(WorldGenLevel level,ChunkGenerator generator,ChunkAccess chunk) {
if(!WorldgenLab.gallery182(generator))return;
var pos=chunk.getPos();if(pos.x()< -1||pos.x()>0||pos.z()< -1||pos.z()>0)return;
plan(level.getSeed()).forEach((p,b)->{if(pos.contains(p))chunk.setBlockState(p,b);});
}
static Map<String,Object> check(ServerLevel level) {
for(int x=-1;x<=0;x++)for(int z=-1;z<=0;z++)level.getChunk(x,z);
var expected=plan(level.getSeed());
for(var e:expected.entrySet())if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("Rosette mismatch at "+e.getKey());
var core=level.getBlockState(Origin181.CORE);
if(!core.is(OriginBlock.BLOCK)||core.getValue(OriginBlock.RELAYS)!=0||!core.getValue(OriginBlock.LIT))throw new IllegalStateException("Missing monochrome core");
for(var d:AXES)for(int step=2;step<=8;step++) {
int x=d[0]*step,z=d[1]*step;if(Math.hypot(x,z)>9)continue;
for(int y=319;y<=321;y++)if(!level.getBlockState(new BlockPos(x,y,z)).isAir())throw new IllegalStateException("Blocked rosette axis "+Arrays.toString(d));
var foot=new BlockPos(x,318,z);if(level.getBlockState(foot).getCollisionShape(level,foot).isEmpty())throw new IllegalStateException("Unsupported rosette approach");
}
return Map.of("openAxes",8,"supportSectors",8,"core",Origin181.CORE.toShortString(),"blocks",expected.size());
}
}
@@ -0,0 +1,35 @@
package fr.koka.sanctuarytest;
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.block.state.BlockState;
/** Same shell and free approaches as 182; sectors follow origin-logo.png clockwise from north. */
final class Rosette183 {
private Rosette183() {}
static final Block[] GLASS={Blocks.STAINED_GLASS.lightBlue(),Blocks.STAINED_GLASS.lime(),
Blocks.STAINED_GLASS.green(),Blocks.STAINED_GLASS.orange(),Blocks.STAINED_GLASS.red(),
Blocks.STAINED_GLASS.magenta(),Blocks.STAINED_GLASS.yellow(),Blocks.STAINED_GLASS.cyan()};
static int sector(int x,int z){return Math.floorMod((int)Math.round(Math.atan2(x,-z)/(Math.PI/4)),8);}
static Map<BlockPos,BlockState> plan(long seed){
var result=new LinkedHashMap<>(Rosette182.plan(seed));
result.replaceAll((p,b)->(b.is(Blocks.GLASS)||b.is(Blocks.STAINED_GLASS.white()))&&Math.hypot(p.getX(),p.getZ())>=2
?GLASS[sector(p.getX(),p.getZ())].defaultBlockState():b);
return result;
}
static Map<String,Object> check(ServerLevel level){
var original=Rosette182.plan(level.getSeed());var colored=plan(level.getSeed());
if(!original.keySet().equals(colored.keySet()))throw new IllegalStateException("Rosette geometry changed");
int[] colors=new int[8];
for(var e:colored.entrySet()){
if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("Colored rosette mismatch "+e.getKey());
for(int i=0;i<8;i++)if(e.getValue().is(GLASS[i]))colors[i]++;
}
for(int n:colors)if(n<8)throw new IllegalStateException("Missing compass glass sector");
for(int x=-8;x<=8;x++)for(int z=-8;z<=8;z++)if(Math.hypot(x,z)>=2&&Math.hypot(x,z)<=9&&(x==0||z==0||Math.abs(x)==Math.abs(z)))
for(int y=319;y<=321;y++)if(!level.getBlockState(new BlockPos(x,y,z)).isAir())throw new IllegalStateException("Blocked rosette direction");
return Map.of("geometryUnchanged",true,"glassClockwiseFromNorth",colors,"core",Origin181.CORE.toShortString());
}
}
@@ -0,0 +1,107 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Small abandoned dwellings admitted only into existing dry, supported cave space. */
public final class Ruins189 {
private static final net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> LOOT190 = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.LOOT_TABLE,net.minecraft.resources.Identifier.fromNamespaceAndPath("sanctuary_test","chests/ruins_cache_190"));
record House(BlockPos floor,int score) {}
record Plan(List<House> houses,Map<ChunkPos,Map<BlockPos,BlockState>> chunks,int probes,long millis) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private static boolean rock(AdventureBiomes180 s,BlockPos p){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())>0;}
static Plan plan(AdventureBiomes180 s){synchronized(PLANS){return PLANS.computeIfAbsent(s,Ruins189::create);}}
private static Plan create(AdventureBiomes180 s){
long start=System.nanoTime();var candidates=new ArrayList<House>();int probes=0;
for(int x=-128;x<=128;x+=8)for(int z=-128;z<=128;z+=8){
if(Math.hypot(x,z)<64||Math.hypot(x,z)>138||Dungeon180.near(s,x,z,18))continue;
boolean avoid=false;
for(var a:Anchors189.plan(s,s.seed()).sites())if(Math.hypot(x-a.floor().getX(),z-a.floor().getZ())<24)avoid=true;
if(avoid)continue;
for(int y=Math.min(212,s.field().surface(x,z)-20);y>=108;y--){
var p=new BlockPos(x,y,z);if(!rock(s,p)||rock(s,p.above())||rock(s,p.above(6)))continue;probes++;
boolean valid=true;
if(s.field().coherent())for(var c:Cavern183.plan(s).cells().values())
if(Math.abs(c.x()-x)<=4&&Math.abs(c.z()-z)<=4&&y+5>=c.y()-2&&y-1<=c.ceiling())valid=false;
for(var b:Basins180.sites(s))if(Math.abs(y-b.y())<16&&Math.hypot(x-b.x(),z-b.z())<76)valid=false;
if(!valid)continue;
for(int dx=-2;dx<=2&&valid;dx++)for(int dz=-2;dz<=2&&valid;dz++){
var q=p.offset(dx,0,dz);if(!rock(s,q.below(2))){valid=false;break;}
for(int h=1;h<=5;h++)if(rock(s,q.above(h))){valid=false;break;}
for(var pool:NaturalWater184.plan(s).pools())if(pool.cells().contains(q.above()))valid=false;
int waterTop=Water189.plan(s).columns().getOrDefault(((long)q.getX()<<32)^(q.getZ()&0xffffffffL),-1);
if(waterTop>=0&&Math.abs(q.getY()-waterTop)<30)valid=false;
}
boolean roof=false;for(int h=9;h<=40;h+=3)if(rock(s,p.above(h))){roof=true;break;}
if(valid&&roof)candidates.add(new House(p,(int)(300-Math.hypot(x,z)-Math.abs(y-170))));
}
}
candidates.sort(Comparator.comparingInt(House::score).reversed());var chosen=new ArrayList<House>();
for(var house:candidates){
if(chosen.stream().anyMatch(h->h.floor.distSqr(house.floor)<12*12))continue;
if(!chosen.isEmpty()&&chosen.getFirst().floor.distSqr(house.floor)>60*60)continue;
chosen.add(house);if(chosen.size()==5)break;
}
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
for(int i=0;i<chosen.size();i++){
var house=chosen.get(i);var p=house.floor;var m=new LinkedHashMap<BlockPos,BlockState>();
for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++){
var q=p.offset(dx,0,dz);
for(int h=-1;h<=0;h++)m.put(q.above(h),(h==0?Blocks.MOSSY_STONE_BRICKS:Blocks.COBBLESTONE).defaultBlockState());
boolean edge=Math.abs(dx)==2||Math.abs(dz)==2,door=dz==-2&&dx==0;
if(edge&&!door){
int height=Math.abs(dx)==2&&Math.abs(dz)==2?3:Math.floorMod(dx+dz+i,3)==0?1:2;
for(int h=1;h<=height;h++)m.put(q.above(h),(h==3?Blocks.CHISELED_STONE_BRICKS:Blocks.STONE_BRICKS).defaultBlockState());
}
if(dz==2&&dx!=0)m.put(q.above(4),Blocks.STONE_BRICK_SLAB.defaultBlockState());
}
m.put(p.offset(1,1,1),((s.field().refined()||s.originalRelief())?Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING,Direction.NORTH):Blocks.CRAFTING_TABLE.defaultBlockState()));
m.put(p.offset(-2,4,-2),Blocks.COPPER_LANTERN.waxed().oxidized().defaultBlockState());
if(i%2==0)m.put(p.offset(1,3,2),Blocks.COBWEB.defaultBlockState());
m.forEach((q,b)->chunks.computeIfAbsent(new ChunkPos(q.getX()>>4,q.getZ()>>4),k->new LinkedHashMap<>()).put(q,b));
}
chunks.replaceAll((k,v)->Map.copyOf(v));return new Plan(List.copyOf(chosen),Map.copyOf(chunks),probes,(System.nanoTime()-start)/1_000_000);
}
static boolean near(AdventureBiomes180 s,BlockPos p){
for(var h:plan(s).houses)if(Math.abs(p.getX()-h.floor.getX())<=7&&Math.abs(p.getZ()-h.floor.getZ())<=7&&Math.abs(p.getY()-h.floor.getY())<12)return true;
return false;
}
public static boolean protects(ChunkGenerator g,BlockPos p){
if(!WorldgenLab.finishedTerrain(g)||p.getY()<100||p.getY()>240||Math.abs(p.getX())>145||Math.abs(p.getZ())>145)return false;
return near(Anchors189.source(g),p);
}
static void place(AdventureBiomes180 s,ChunkAccess c){
var m=plan(s).chunks.get(c.getPos());if(m==null)return;
m.forEach((p,b)->{
c.setBlockState(p,b);
if((s.field().refined()||s.originalRelief())&&b.is(Blocks.CHEST)){
var chest=new net.minecraft.world.level.block.entity.ChestBlockEntity(p,b);
chest.setLootTable(LOOT190);chest.setLootTableSeed(s.seed()^p.asLong()^0x190L);c.setBlockEntity(chest);
}
});
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var p=plan(s);for(var cp:p.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var e:p.chunks.values())for(var b:e.entrySet())if(!level.getBlockState(b.getKey()).equals(b.getValue()))throw new IllegalStateException("Ruin overwritten "+b.getKey());
for(var h:p.houses)for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++){
var q=h.floor.offset(dx,0,dz);if(!rock(s,q.below(2)))throw new IllegalStateException("Unsupported ruin "+q);
for(int y=1;y<=5;y++)if(rock(s,q.above(y)))throw new IllegalStateException("Ruin cuts cave rock "+q);
}
if((s.field().refined()||s.originalRelief())){
for(var h:p.houses){
var at=h.floor.offset(1,1,1);
if(!(level.getBlockEntity(at) instanceof net.minecraft.world.level.block.entity.ChestBlockEntity chest)||!LOOT190.equals(chest.getLootTable()))throw new IllegalStateException("Missing ruin loot "+at);
if(!level.getBlockState(at.above()).isAir())throw new IllegalStateException("Blocked chest lid "+at);
}
if(p.houses.isEmpty())throw new IllegalStateException("No ruin chests");
return Map.of("houses",p.houses,"chests",p.houses.size(),"ironLoot",IronLoot188.check(level,"chests/ruins_cache_190"),"carvedBlocks",0,"planningMs",p.millis);
}
return Map.of("houses",p.houses,"carvedBlocks",0,"probes",p.probes,"planningMs",p.millis);
}
}
@@ -0,0 +1,43 @@
package fr.koka.sanctuarytest;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import fr.koka.sanctuary.worldgen.IslandCapacity;
/** Versioned shareable laboratory sizes. Historical profiles keep their settings and IDs. */
public final class SharedIsland196 {
private SharedIsland196() {}
public enum Size {
SMALL(512, IslandCapacity.FIVE), MEDIUM(724, IslandCapacity.TEN), LARGE(1024, IslandCapacity.TWENTY);
public final int diameter;
public final IslandCapacity capacity;
private final Identifier id;
private final Identifier[] revisions=new Identifier[8];
Size(int diameter, IslandCapacity capacity) {
this.diameter=diameter;this.capacity=capacity;
for(int revision=196;revision<=203;revision++)revisions[revision-196]=Identifier.fromNamespaceAndPath("sanctuary_test","shared_island"+revision+"_"+name().toLowerCase(java.util.Locale.ROOT));
id=revisions[7];
}
public Identifier id() { return id; }
public Identifier id(int revision) {
if(revision<196||revision>203)throw new IllegalArgumentException("Unknown island revision "+revision);
return revisions[revision-196];
}
}
private static final Size[] SIZES=Size.values();
/** The beta.197 spawn and decoration protections also apply to later profiles. */
public static boolean current(ChunkGenerator generator) { return revision(generator)>=197; }
public static int revision(ChunkGenerator generator) {
if(generator instanceof NoiseBasedChunkGenerator noise)
for(int revision=203;revision>=196;revision--)for(var size:SIZES)
if(noise.generatorSettings().is(size.id(revision)))return revision;
return -1;
}
public static Size size(ChunkGenerator generator) {
if(generator instanceof NoiseBasedChunkGenerator noise)
for(var size:SIZES)for(int revision=203;revision>=196;revision--)
if(noise.generatorSettings().is(size.id(revision)))return size;
return null;
}
}
@@ -0,0 +1,58 @@
package fr.koka.sanctuarytest;
import fr.koka.sanctuary.client.SanctuaryWorldEditors;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.CycleButton;
import net.minecraft.client.gui.components.MultiLineTextWidget;
import net.minecraft.client.gui.layouts.HeaderAndFooterLayout;
import net.minecraft.client.gui.layouts.LinearLayout;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
import net.minecraft.client.gui.screens.worldselection.WorldCreationContext;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
/** Changes only the pending world's generator, never an existing world's terrain. */
public final class SharedIsland196Screen extends Screen {
private final CreateWorldScreen parent;
private final HeaderAndFooterLayout layout=new HeaderAndFooterLayout(this);
private SharedIsland196.Size selected;
private final int revision;
private MultiLineTextWidget hint;
public static void register() {
SanctuaryWorldEditors.register(state -> SharedIsland196.size(state.getSettings().selectedDimensions().overworld())!=null,
SharedIsland196Screen::new);
}
public SharedIsland196Screen(CreateWorldScreen parent, WorldCreationContext context) {
super(Component.translatable("sanctuary.world_options.title"));
this.parent=parent;
revision=SharedIsland196.revision(context.selectedDimensions().overworld());
selected=java.util.Objects.requireNonNull(SharedIsland196.size(context.selectedDimensions().overworld()));
}
@Override protected void init() {
layout.removeChildren();layout.addTitleHeader(title,font);
var contents=LinearLayout.vertical().spacing(12);
contents.defaultCellSetting().alignHorizontallyCenter();
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary_test.shared196.description"),font).setMaxWidth(310).setCentered(true));
contents.addChild(CycleButton.builder((SharedIsland196.Size size)->Component.translatable("sanctuary.world_options.size."+size.name().toLowerCase(java.util.Locale.ROOT)),selected)
.withValues(SharedIsland196.Size.values()).create(0,0,240,20,Component.translatable("sanctuary.world_options.size"),(button,size)->{
selected=size;hint.setMessage(diameter());layout.arrangeElements();
}));
hint=contents.addChild(new MultiLineTextWidget(diameter(),font).setMaxWidth(310).setCentered(true));
layout.addToContents(contents);
var actions=LinearLayout.horizontal().spacing(8);
actions.addChild(Button.builder(CommonComponents.GUI_DONE,button->{
var preset=parent.getUiState().getSettings().worldgenLoadContext().lookupOrThrow(Registries.WORLD_PRESET)
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,selected.id(revision)));
parent.getUiState().updateDimensions((registries,dimensions)->dimensions.replaceOverworldGenerator(registries,preset.value().createWorldDimensions().overworld()));
onClose();
}).width(150).build());
actions.addChild(Button.builder(CommonComponents.GUI_CANCEL,button->onClose()).width(150).build());
layout.addToFooter(actions);layout.visitWidgets(this::addRenderableWidget);repositionElements();
}
private Component diameter(){return Component.translatable("sanctuary.world_options.diameter",selected.diameter);}
@Override protected void repositionElements(){layout.arrangeElements();}
@Override public void onClose(){minecraft.gui.setScreen(parent);}
}
@@ -21,6 +21,10 @@ public record SkyFragments175(DensityFunction ground, DensityFunction coarse, De
public record Reef(int x, int y, int z, int length, int width, int depth, int rise, double angle) {}
@Override public DensitySampler compileSampler(CompileContext context) {
var island = DfRewriteRule.INLINE_REFERENCE.rewrite(ground);
if (!(island instanceof PopulationIslandDensity population))
throw new IllegalStateException("Sky v1 requires the accepted population island field");
double spread=population.capacity().radius()/fr.koka.sanctuary.worldgen.IslandCapacity.TEN.radius();
var random = context.createRandom(Identifier.fromNamespaceAndPath("sanctuary_test", "sky_v1"));
var reefs = new ArrayList<Reef>();
double phase = random.nextDouble() * Math.PI * 2;
@@ -28,7 +32,7 @@ public record SkyFragments175(DensityFunction ground, DensityFunction coarse, De
boolean high = i >= 3;
double bearing = phase + (high ? (i - 3) * Math.PI + .8 : i * Math.PI * 2 / 3)
+ (random.nextDouble() - .5) * .35;
double distance = high ? 175 + random.nextInt(85) : 170 + random.nextInt(75);
double distance = (high ? 175 + random.nextInt(85) : 170 + random.nextInt(75))*spread;
reefs.add(new Reef((int)Math.round(Math.cos(bearing) * distance),
high ? 492 + random.nextInt(8) : 362 + i * 40 + random.nextInt(8),
(int)Math.round(Math.sin(bearing) * distance),
@@ -37,22 +41,23 @@ public record SkyFragments175(DensityFunction ground, DensityFunction coarse, De
high ? 8 + random.nextInt(4) : 15 + random.nextInt(6),
high ? 3 : 5, bearing + .5 + random.nextDouble()));
}
var island = DfRewriteRule.INLINE_REFERENCE.rewrite(ground);
if (!(island instanceof PopulationIslandDensity population))
throw new IllegalStateException("Sky v1 requires the accepted population island field");
return new Field(population.compileWithoutUpperBodies(context), coarse.compileSampler(context),
detail.compileSampler(context), List.copyOf(reefs));
detail.compileSampler(context), List.copyOf(reefs), population.capacity());
}
public static final class Field implements DensitySampler {
private final DensitySampler ground, coarse, detail;
private final List<Reef> reefs;
private final fr.koka.sanctuary.worldgen.IslandCapacity capacity;
private final double[] cos, sin;
Field(DensitySampler ground, DensitySampler coarse, DensitySampler detail, List<Reef> reefs) {
this.ground=ground; this.coarse=coarse; this.detail=detail; this.reefs=reefs;
Field(DensitySampler ground, DensitySampler coarse, DensitySampler detail, List<Reef> reefs, fr.koka.sanctuary.worldgen.IslandCapacity capacity) {
this.ground=ground; this.coarse=coarse; this.detail=detail; this.reefs=reefs; this.capacity=capacity;
cos=new double[reefs.size()]; sin=new double[reefs.size()];
for (int i=0;i<reefs.size();i++) {cos[i]=Math.cos(reefs.get(i).angle());sin[i]=Math.sin(reefs.get(i).angle());}
}
public fr.koka.sanctuary.worldgen.IslandCapacity capacity(){return capacity;}
public double spread(){return capacity.radius()/fr.koka.sanctuary.worldgen.IslandCapacity.TEN.radius();}
public int terrainBound(){return capacity.terrainLimit()+3;}
public List<Reef> reefs() { return reefs; }
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
@@ -0,0 +1,103 @@
package fr.koka.sanctuarytest;
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.block.state.BlockState;
/** Original ISS silhouette: connected pressure modules, service truss, four photovoltaic wings and nadir cupola. */
final class Station183 {
private Station183() {}
private static void put(Map<BlockPos,BlockState> m,int x,int y,int z,Block b){m.put(new BlockPos(x,y,z),b.defaultBlockState());}
private static void module(Map<BlockPos,BlockState> m,int centerZ,int length,int radius){
for(int z=centerZ-length;z<=centerZ+length;z++)for(int x=-radius;x<=radius;x++)for(int h=0;h<=6;h++){
if(Math.abs(x)==radius&&(h==0||h==6))continue;
boolean shell=Math.abs(x)==radius||h==0||h==6||Math.abs(z-centerZ)==length;
Block b=!shell?Blocks.AIR:(h==0||h==6)?Blocks.IRON_BLOCK:Blocks.SMOOTH_QUARTZ;
if(shell&&Math.abs(x)==radius&&h>=2&&h<=4&&Math.floorMod(z-centerZ,5)<3)b=Blocks.TINTED_GLASS;
if(shell&&h==1&&(Math.abs(z-centerZ)==length||Math.abs(x)==radius))b=Blocks.POLISHED_ANDESITE;
put(m,x,576+h,z,b);
}
}
record Layout(int tiles,int radius) {
int half(){return tiles/2;}
int roomRadius(){return half()+3;}
}
static Layout layout(int terrainRadius){
int half=Math.max(1,(int)Math.ceil((terrainRadius-63)/128.0));
if(half>8)throw new IllegalArgumentException("ISS atlas exceeds supported laboratory bounds");
return new Layout(half*2+1,terrainRadius);
}
static Layout layout(net.minecraft.world.level.chunk.ChunkGenerator generator){
var noise=(net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator)generator;
var root=net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule.INLINE_REFERENCE.rewrite(noise.generatorSettings().value().noiseRouter().finalDensity());
var relief=(ReliefEcology177)root;
var sky=(SkyFragments175)net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule.INLINE_REFERENCE.rewrite(relief.base());
var ground=(fr.koka.sanctuary.worldgen.PopulationIslandDensity)net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule.INLINE_REFERENCE.rewrite(sky.ground());
return layout(ground.capacity().terrainLimit());
}
static Map<BlockPos,BlockState> plan(Layout layout){
var m=new LinkedHashMap<BlockPos,BlockState>();
module(m,0,6,5);module(m,-18,layout.half()+4,layout.roomRadius());module(m,18,7,3);
// Two sealed, traversable links. Interiors are explicitly air, including module end caps.
for(int sign:new int[]{-1,1})for(int z=sign<0?-12:5;z<=(sign<0?-5:12);z++)for(int x=-2;x<=2;x++)for(int h=0;h<=4;h++)
put(m,x,576+h,z,Math.abs(x)==2||h==0||h==4?Blocks.IRON_BLOCK:Blocks.AIR);
// Four thin blue-black solar arrays mounted on an open metal truss.
for(int sign:new int[]{-1,1}){
for(int a=6;a<=43;a++){
int x=sign*a;put(m,x,578,0,Blocks.IRON_BLOCK);put(m,x,580,0,Blocks.IRON_BARS);
if(a%4==0)for(int z=-2;z<=2;z++)put(m,x,579,z,Blocks.IRON_BARS);
}
for(int bank:new int[]{-1,1})for(int a=15;a<=40;a++)for(int b=4;b<=15;b++){
boolean border=a==15||a==40||b==4||b==15||a%7==0;
put(m,sign*a,580,bank*b,border?Blocks.CONCRETE.black():Blocks.CONCRETE.blue());
if(a%7==0&&b<=5)put(m,sign*a,579,bank*(b-2),Blocks.IRON_BARS);
}
}
// Downward-facing glazed floor: the island, origin and compass can be read below.
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)if(x*x+z*z<=10){
put(m,x,576,z,Blocks.GLASS);
if(x*x+z*z>=5)put(m,x,575,z,Rosette183.GLASS[Rosette183.sector(x,z)]);
}
for(int z:new int[]{-23,-18,-13,0,13,18,23})put(m,0,582,z,Blocks.SEA_LANTERN);
// Inhabited traces, with free central aisles. No quest rewards, guardians or active portal.
for(int z:new int[]{14,17,20}){put(m,-2,577,z,Blocks.CRAFTING_TABLE);put(m,2,577,z,Blocks.BARREL);}
// Raised atlas table and a clear ring to inspect it from all four sides.
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++)
put(m,x,577,-18+z,Blocks.QUARTZ_BLOCK);
// Docking porch and a three-block-wide open hatch toward the origin below.
for(int x=-3;x<=3;x++)for(int z=26;z<=30;z++){
put(m,x,576,z,Blocks.IRON_BLOCK);
if(Math.abs(x)==3||z==30)put(m,x,577,z,Blocks.IRON_BARS);
}
for(int x=-1;x<=1;x++)for(int y=577;y<=579;y++)put(m,x,y,25,Blocks.AIR);
// Communications mast and small dish above the laboratory module.
for(int y=583;y<=590;y++)put(m,0,y,18,Blocks.IRON_BARS);
for(int x=-2;x<=2;x++)for(int z=16;z<=20;z++)if(x*x+(z-18)*(z-18)<=5)put(m,x,589,z,Blocks.QUARTZ_SLAB);
put(m,0,591,18,Blocks.LIGHTNING_ROD.waxed().oxidized());
return Collections.unmodifiableMap(m);
}
static Map<String,Object> check(ServerLevel level){
var layout=layout(level.getChunkSource().getGenerator());var p=plan(layout);int min=640,max=0;
for(var e:p.entrySet()){
if(!level.getBlockState(e.getKey()).equals(e.getValue()))throw new IllegalStateException("ISS block mismatch "+e.getKey());
min=Math.min(min,e.getKey().getY());max=Math.max(max,e.getKey().getY());
}
for(int z=-10;z<=29;z++){
var at=new BlockPos(0,577,z);
if(!level.getBlockState(at).isAir()||!level.getBlockState(at.above()).isAir()||level.getBlockState(at.below()).getCollisionShape(level,at.below()).isEmpty())throw new IllegalStateException("ISS aisle blocked "+at);
}
// The three supported world sizes scale both their tables and pressure modules.
int previous=0;
for(int radius:new int[]{288,395,544}){
var variant=layout(radius);var blocks=plan(variant);
if(variant.tiles()<=previous||variant.half()*128+63<radius)throw new IllegalStateException("ISS atlas size/coverage regression");
for(int x=-variant.half();x<=variant.half();x++)for(int z=-variant.half();z<=variant.half();z++)
if(!blocks.get(new BlockPos(x,577,-18+z)).is(Blocks.QUARTZ_BLOCK))throw new IllegalStateException("Unsupported map table");
previous=variant.tiles();
}
if(min<512||max>=640)throw new IllegalStateException("ISS outside reserved heights");
return Map.of("arrival","0, 577, 28","altitudeMin",min,"altitudeMax",max,"planBlocks",p.size(),"connectedModules",3,"solarWings",4,"atlasTiles",layout.tiles()*layout.tiles());
}
}
@@ -0,0 +1,114 @@
package fr.koka.sanctuarytest;
import java.util.*;
import java.nio.charset.StandardCharsets;
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.decoration.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.saveddata.maps.*;
/** Native saved maps in native horizontal frames. Bounded preview jobs never request remote chunks. */
final class StationAtlas183 {
private static final String TAG="sanctuary_test:iss_atlas183",PENDING=TAG+"_pending";
private static final Map<ServerLevel,LinkedHashMap<UUID,Job>> JOBS=new WeakHashMap<>();
private static final class Job {
final ItemFrame frame;final MapId id;final MapItemSavedData data;int row;
Job(ItemFrame frame,MapId id,MapItemSavedData data){this.frame=frame;this.id=id;this.data=data;}
}
private StationAtlas183() {}
static void register(){
ServerEntityEvents.ENTITY_LOAD.register((entity,level)->{if(entity instanceof ItemFrame frame&&frame.entityTags().contains(PENDING)&&WorldgenLab.discovery183(level.getChunkSource().getGenerator()))enqueue(level,frame);});
ServerTickEvents.END_SERVER_TICK.register(server->{var level=server.overworld();long until=System.nanoTime()+3_000_000;while(hasWork(level)&&System.nanoTime()<until)step(level);});
ServerLifecycleEvents.SERVER_STOPPED.register(server->JOBS.remove(server.overworld()));
}
static void populate(WorldGenLevel level,ChunkGenerator generator,ChunkAccess c){
var layout=Station183.layout(generator);
if(c.getPos().x()< -1||c.getPos().x()>0||c.getPos().z()< -2||c.getPos().z()>-1)return;
for(int x=-layout.half();x<=layout.half();x++)for(int z=-layout.half();z<=layout.half();z++){
var at=new BlockPos(x,578,-18+z);if(!c.getPos().contains(at))continue;
var frame=new GlowItemFrame(level.getLevel(),at,Direction.UP);
frame.setUUID(UUID.nameUUIDFromBytes((TAG+":"+level.getSeed()+":"+x+":"+z).getBytes(StandardCharsets.UTF_8)));
frame.addTag(TAG);frame.addTag(PENDING);level.addFreshEntity(frame);
}
}
private static void enqueue(ServerLevel level,ItemFrame frame){
var queue=JOBS.computeIfAbsent(level,l->new LinkedHashMap<>());if(queue.containsKey(frame.getUUID()))return;
var p=frame.blockPosition();var stack=frame.getItem();
if(!stack.is(Items.FILLED_MAP)){
stack=MapItem.create(level,p.getX()*128,(p.getZ()+18)*128,(byte)0,false,false);
stack.set(DataComponents.ITEM_NAME,Component.translatable("item.sanctuary_test.iss_chart",p.getX(),p.getZ()+18));frame.setItem(stack,false);frame.setRotation(0);
}
var id=stack.get(DataComponents.MAP_ID);var data=MapItem.getSavedData(stack,level);
if(data==null)throw new IllegalStateException("Missing ISS native map data");
queue.put(frame.getUUID(),new Job(frame,id,data));
}
private static boolean hasWork(ServerLevel l){var q=JOBS.get(l);return q!=null&&!q.isEmpty();}
private static void step(ServerLevel level){
var q=JOBS.get(level);var entry=q.entrySet().iterator().next();var job=entry.getValue();
var source=(AdventureBiomes180)((NoiseBasedChunkGenerator)level.getChunkSource().getGenerator()).getBiomeSource();
// Four map rows per slice, terrain sampled at quart resolution and expanded in the native raster.
for(int x=0;x<128;x+=4){
int wx=job.data.centerX-64+x,wz=job.data.centerZ-64+job.row;
byte color=color(level,source,wx,wz);
for(int dx=0;dx<4;dx++)for(int dz=0;dz<4;dz++)job.data.setColor(x+dx,job.row+dz,color);
}
job.row+=4;
if(job.row==128){level.setMapData(job.id,job.data.locked());job.frame.removeTag(PENDING);q.remove(entry.getKey());}
}
private static byte color(ServerLevel level,AdventureBiomes180 s,int x,int z){
int y=s.field().surface(x,z);
if(y<0)return MapColor.COLOR_BLACK.getPackedId(MapColor.Brightness.LOW);
MapColor color;
// Already loaded chunks provide their real ground palette; no new chunk is ever requested here.
var chunk=level.getChunkSource().getChunkNow(x>>4,z>>4);
if(chunk!=null){
int top=Math.min(316,chunk.getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.WORLD_SURFACE,x&15,z&15)+1);
BlockPos p=new BlockPos(x,top,z);
while(p.getY()>0&&(chunk.getBlockState(p).isAir()||chunk.getBlockState(p).getMapColor(level,p)==MapColor.NONE))p=p.below();
color=chunk.getBlockState(p).getMapColor(level,p);
if(color==MapColor.NONE)color=MapColor.COLOR_BLACK;
}else{
String biome=s.resolve(x,y,z).unwrapKey().orElseThrow().identifier().getPath();
color=switch(biome){case "adventure_autumn"->MapColor.COLOR_ORANGE;case "adventure_cliffs"->MapColor.STONE;case "adventure_forest"->MapColor.PLANT;default->MapColor.GRASS;};
}
int north=s.field().surface(x,z-4);
var shade=north<0||y==north?MapColor.Brightness.NORMAL:y>north?MapColor.Brightness.HIGH:MapColor.Brightness.LOW;
// Surface clue remains visible even in the inexpensive projected part of the chart.
var sulfur=Sulfur183.site(s);if(Math.hypot(x-sulfur.x(),z-sulfur.z())<12)color=MapColor.COLOR_YELLOW;
return color.getPackedId(shade);
}
static Map<String,Object> check(ServerLevel level){
var layout=Station183.layout(level.getChunkSource().getGenerator());
var manager=((fr.koka.sanctuary.mixin.ExpansionVoidServerLevelAccess)level).sanctuary$entityManager();
var chunks=new ArrayList<net.minecraft.world.level.ChunkPos>();
for(int x=-1;x<=0;x++)for(int z=-2;z<=-1;z++){
var cp=new net.minecraft.world.level.ChunkPos(x,z);chunks.add(cp);level.getChunk(x,z);
manager.updateChunkStatus(cp,net.minecraft.world.level.entity.Visibility.TRACKED);
}
long deadline=System.nanoTime()+5_000_000_000L;
level.getServer().managedBlock(()->{manager.processPendingLoads();return chunks.stream().allMatch(cp->manager.areEntitiesLoaded(cp.pack()))||System.nanoTime()>deadline;});
if(chunks.stream().anyMatch(cp->!manager.areEntitiesLoaded(cp.pack())))throw new IllegalStateException("ISS entity loading timed out");
var frames=new ArrayList<ItemFrame>();
for(var entity:level.getAllEntities())if(entity instanceof ItemFrame f&&f.entityTags().contains(TAG)){frames.add(f);if(f.entityTags().contains(PENDING))enqueue(level,f);}
if(frames.size()!=layout.tiles()*layout.tiles())throw new IllegalStateException("Missing atlas frames: "+frames.size());
long started=System.nanoTime();while(hasWork(level))step(level);
var ids=new HashSet<MapId>();int painted=0;
for(var frame:frames){
if(frame.getDirection()!=Direction.UP||!frame.survives())throw new IllegalStateException("Unsupported horizontal frame "+frame.blockPosition());
var id=frame.getItem().get(DataComponents.MAP_ID);var data=level.getMapData(id);var p=frame.blockPosition();
if(!ids.add(id)||data==null||!data.locked||data.centerX!=p.getX()*128||data.centerZ!=(p.getZ()+18)*128)throw new IllegalStateException("Atlas tile continuity/persistence mismatch");
for(byte b:data.colors)if(b!=0)painted++;
}
if(painted!=frames.size()*128*128)throw new IllegalStateException("Blank chart pixels");
return Map.of("frames",frames.size(),"tilesPerSide",layout.tiles(),"mapScale",0,"coveredDiameter",layout.tiles()*128,"previewMs",(System.nanoTime()-started)/1_000_000,"remoteChunkLoads",0,"tableCenter","0, 578, -18");
}
}
@@ -0,0 +1,67 @@
package fr.koka.sanctuarytest;
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.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** One bounded mineral cavern with a daylight vent and rooted surface chimneys. No fluid planner. */
final class Sulfur183 {
record Site(int x,int y,int z,int surface) {}
private static final Map<AdventureBiomes180,Site> SITES=Collections.synchronizedMap(new WeakHashMap<>());
private Sulfur183() {}
static Site site(AdventureBiomes180 s){synchronized(SITES){return SITES.computeIfAbsent(s,key->{
int x=-152,z=152,top=s.terrain(x,z).top(),best=Math.max(72,top-62),score=Integer.MIN_VALUE;
for(int y=90;y<=top-28;y+=2){
int candidate=0;
for(int dx=-12;dx<=12;dx+=6)for(int dz=-12;dz<=12;dz+=6){
if(rock(s,x+dx,y-4,z+dz))candidate+=2;
if(!rock(s,x+dx,y+5,z+dz))candidate++;
if(rock(s,x+dx,y+22,z+dz))candidate++;
}
if(candidate>score){score=candidate;best=y;}
}
return new Site(x,best,z,top);
});}}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
static boolean biome(AdventureBiomes180 s,int x,int y,int z){var p=site(s);return Math.pow((x-p.x)/30.0,2)+Math.pow((z-p.z)/25.0,2)<1&&y>=p.y-8&&y<=p.y+24;}
private static void put(ChunkAccess c,int x,int y,int z,Block b){var p=new BlockPos(x,y,z);if(c.getPos().contains(p))c.setBlockState(p,b.defaultBlockState());}
static void place(AdventureBiomes180 s,ChunkAccess c){
var p=site(s);if(Math.abs(c.getPos().getMiddleBlockX()-p.x)>44||Math.abs(c.getPos().getMiddleBlockZ()-p.z)>40)return;
for(int x=c.getPos().getMinBlockX();x<=c.getPos().getMaxBlockX();x++)for(int z=c.getPos().getMinBlockZ();z<=c.getPos().getMaxBlockZ();z++){
double u=x-p.x,v=z-p.z;
double shape=u*u/625+v*v/400+.08*Math.sin(x*.33)*Math.cos(z*.24);
if(shape<=1){
int floor=p.y+(int)Math.round(Math.sin(u*.14)+Math.cos(v*.18));
int roof=floor+5+(int)(13*Math.sqrt(Math.max(0,1-shape)));
for(int y=floor-3;y<=roof+2;y++){
Block b=y>floor&&y<=roof?Blocks.AIR:Math.floorMod(x*7+z*11+y/3,13)<2?Blocks.CINNABAR:Math.floorMod(x+z+y,7)<5?Blocks.SULFUR:Blocks.CALCITE;
put(c,x,y,z,b);
}
}
// A small skewed skylight opens the room; the whole surface is not excavated.
if(Math.abs(u-7)<=1&&Math.abs(v+3)<=1)
for(int y=p.y+3;y<=s.terrain(x,z).top()+8;y++)put(c,x,y,z,Blocks.AIR);
}
for(int[] offset:new int[][]{{-6,-3,9},{5,7,6},{10,-7,8}}){
int x=p.x+offset[0],z=p.z+offset[1],top=s.terrain(x,z).top();
for(int h=-3;h<=offset[2];h++)for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++){
int radius=h>offset[2]-3?0:h>2?1:2;if(dx*dx+dz*dz>radius*radius+1)continue;
put(c,x+dx,top+h,z+dz,Math.floorMod(dx+dz+h,7)==0?Blocks.CINNABAR:Blocks.SULFUR);
}
}
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var p=site(s);for(int x=(p.x-32)>>4;x<=(p.x+32)>>4;x++)for(int z=(p.z-28)>>4;z<=(p.z+28)>>4;z++)level.getChunk(x,z);
var inside=new BlockPos(p.x,p.y+4,p.z);
if(!level.getBlockState(inside).isAir())throw new IllegalStateException("Sulfur room obstructed");
var marker=new BlockPos(p.x-6,s.terrain(p.x-6,p.z-3).top()+8,p.z-3);
if(!level.getBlockState(marker).is(Blocks.SULFUR))throw new IllegalStateException("Missing visible sulfur chimney");
int ventTop=s.terrain(p.x+7,p.z-3).top()+6;
for(int y=p.y+4;y<=ventTop;y++)if(!level.getBlockState(new BlockPos(p.x+7,y,p.z-3)).isAir())throw new IllegalStateException("Sulfur vent blocked");
if(!level.getBiome(inside).unwrapKey().orElseThrow().identifier().getPath().equals("discovery_sulfur"))throw new IllegalStateException("Sulfur biome missing");
return Map.of("cavern",inside.toShortString(),"surfaceMarker",marker.toShortString(),"surfaceVent",new BlockPos(p.x+7,ventTop,p.z-3).toShortString());
}
}
@@ -0,0 +1,48 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** A three-dimensional cave climate. It changes geology, never the island's density. */
final class Sulfur187 {
private Sulfur187() {}
static boolean biome(long seed,int x,int y,int z,int top){
// The wet marsh climate keeps priority, including its existing witch refuge.
if(AdventureBiomes180.moisture(seed,x,y,z)>=-.30 && AdventureBiomes180.moisture(seed^0x179B10L,x+71,y,z-93)>.25)return false;
return y>=64&&y<top-16&&Math.hypot(x,z)<310
&&AdventureBiomes180.moisture(seed^0x1875A1FL,x,y,z)>.30;
}
private static boolean rock(AdventureBiomes180 s,BlockPos p){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())>0;}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var sites=new ArrayList<BlockPos>();var details=new ArrayList<Map<String,Object>>();
// Find actual natural floor/air interfaces well inside the climate volume, not artificial rooms.
for(int x=-276;x<=276&&sites.size()<4;x+=12)for(int z=-276;z<=276&&sites.size()<4;z+=12){
if(Math.hypot(x,z)<70||Dungeon180.near(s,x,z,36))continue;
int top=s.field().surface(x,z);
for(int y=76;y<Math.min(236,top-24);y++){
var p=new BlockPos(x,y,z);
if(!biome(s.seed(),x,y,z,top)||!biome(s.seed(),x,y+8,z,top)||!rock(s,p)||rock(s,p.above())||rock(s,p.above(3)))continue;
if(sites.stream().anyMatch(a->a.distSqr(p)<70*70))continue;
level.getChunk(x>>4,z>>4);
if(!level.getBlockState(p.above()).isAir()||!level.getBlockState(p.above(2)).isAir())continue;
String b=level.getBiome(p.above()).unwrapKey().orElseThrow().identifier().getPath();
if(!b.equals("natural_sulfur_187"))continue;
int sulfur=0;
for(int dx=-5;dx<=5;dx++)for(int dz=-5;dz<=5;dz++)for(int dy=-3;dy<=8;dy++){
var q=p.offset(dx,dy,dz);var block=level.getBlockState(q);
if(block.is(Blocks.SULFUR)||block.is(Blocks.CINNABAR)){
if(!rock(s,q))throw new IllegalStateException("Sulfur created rock in natural void "+q);
sulfur++;
}
}
if(sulfur<30)continue;
sites.add(p);details.add(Map.of("floor",p.toShortString(),"naturalMineralBlocks",sulfur,"biome",b));break;
}
}
if(sites.size()<2)throw new IllegalStateException("Expected multiple natural sulfur cave regions, got "+details);
return Map.of("caves",details,"carvedRock",0,"artificialShell",0,"surfaceChimneys",0);
}
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Native sulfur spikes and functional, rock-contained geothermal sources. No shell or cavern carving. */
final class SulfurDetails188 {
record Plan(Map<ChunkPos,Map<BlockPos,BlockState>> chunks,List<BlockPos> geysers,int spikes) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private static boolean rock(AdventureBiomes180 s,BlockPos p){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,p.getX(),p.getY(),p.getZ())>0;}
static Plan plan(AdventureBiomes180 s){synchronized(PLANS){return PLANS.computeIfAbsent(s,SulfurDetails188::create);}}
private static Plan create(AdventureBiomes180 s){
var blocks=new HashMap<BlockPos,BlockState>();var geysers=new ArrayList<BlockPos>();int spikes=0;
for(int gx=-280;gx<=280;gx+=8)for(int gz=-280;gz<=280;gz+=8){
long grain=net.minecraft.util.Mth.getSeed(new BlockPos(gx,188,gz))^s.seed();
int x=gx+(int)Math.floorMod(grain,5)-2,z=gz+(int)Math.floorMod(grain>>>8,5)-2;
int top=s.field().surface(x,z);if(top<96||Math.hypot(x,z)<65||Dungeon180.near(s,x,z,18))continue;
boolean anchor=false;if((s.field().terraces()||(s.field().refined()||s.originalRelief()))){for(var a:Anchors189.plan(s,s.seed()).sites())if(Math.hypot(x-a.floor().getX(),z-a.floor().getZ())<20){anchor=true;break;}}
else for(var a:Anchors188.plan(s,s.seed()).sites())if(Math.hypot(x-a.floor().getX(),z-a.floor().getZ())<20){anchor=true;break;}if(anchor)continue;
for(int y=76;y<Math.min(top-20,240);y++){
var floor=new BlockPos(x,y,z);
if(!Sulfur187.biome(s.seed(),x,y,z,top)||!rock(s,floor)||rock(s,floor.above())||rock(s,floor.above(6)))continue;
if(s.field().coherent()&&Ruins189.near(s,floor))continue;
boolean wet=false;
for(var b:Basins180.sites(s))if(Math.abs(y-b.y())<24&&Math.hypot(x-b.x(),z-b.z())<76)wet=true;
for(var b:NaturalWater184.plan(s).pools())if(b.cells().contains(floor)||b.cells().contains(floor.above()))wet=true;
for(var b:SurfaceWater187.plan(s).pools())if(b.cells().contains(floor)||b.cells().contains(floor.above()))wet=true;
if(wet)continue;
int roof=-1;for(int h=7;h<=44&&y+h<top;h++)if(rock(s,floor.above(h))){roof=h;break;}
if(roof<8)continue;
boolean sealed=rock(s,floor.below())&&rock(s,floor.below(2))&&rock(s,floor.below(3));
for(var d:Direction.Plane.HORIZONTAL)if(!rock(s,floor.relative(d)))sealed=false;
if(sealed&&Math.floorMod(grain,4)==0){
blocks.put(floor.below(2),Blocks.MAGMA_BLOCK.defaultBlockState());
blocks.put(floor.below(),Blocks.POTENT_SULFUR.defaultBlockState().setValue(PotentSulfurBlock.STATE,PotentSulfurState.DORMANT));
blocks.put(floor,Blocks.WATER.defaultBlockState());geysers.add(floor);
}else {
int length=2+(int)Math.floorMod(grain,4);length=Math.min(length,(roof-2)/2);
blocks.put(floor,Blocks.SULFUR.defaultBlockState());
blocks.put(floor.above(roof),Blocks.SULFUR.defaultBlockState());
for(var direction:new Direction[]{Direction.UP,Direction.DOWN}){
var base=direction==Direction.UP?floor:floor.above(roof);
for(int h=1;h<=length;h++)blocks.put(base.relative(direction,h),Blocks.SULFUR_SPIKE.defaultBlockState()
.setValue(SpeleothemBlock.TIP_DIRECTION,direction)
.setValue(SpeleothemBlock.THICKNESS,h==length?SpeleothemThickness.TIP:h==length-1?SpeleothemThickness.FRUSTUM:h==1?SpeleothemThickness.BASE:SpeleothemThickness.MIDDLE));
spikes++;
}
}
break;
}
}
var chunks=new HashMap<ChunkPos,Map<BlockPos,BlockState>>();
blocks.forEach((p,b)->chunks.computeIfAbsent(new ChunkPos(p.getX()>>4,p.getZ()>>4),k->new HashMap<>()).put(p,b));
chunks.replaceAll((k,v)->Map.copyOf(v));return new Plan(Map.copyOf(chunks),List.copyOf(geysers),spikes);
}
static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess c){
var entries=plan(Anchors188.source(g)).chunks.get(c.getPos());if(entries==null)return;
entries.forEach((p,b)->{c.setBlockState(p,b);if(b.is(Blocks.POTENT_SULFUR))c.setBlockEntity(new net.minecraft.world.level.block.entity.PotentSulfurBlockEntity(p,b));if(b.is(Blocks.WATER)||b.is(Blocks.POTENT_SULFUR))c.markPosForPostProcessing(p);});
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var p=plan(s);int tips=0;
// Sample several separate chunks and every geyser; native ticking owns eruption state.
var sample=p.chunks.keySet().stream().sorted(Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).limit(12).toList();
for(var cp:sample){level.getChunk(cp.x(),cp.z());for(var e:p.chunks.get(cp).entrySet())if(e.getValue().is(Blocks.SULFUR_SPIKE)){
if(!level.getBlockState(e.getKey()).is(Blocks.SULFUR_SPIKE))throw new IllegalStateException("Missing native sulfur spike "+e.getKey());tips++;
}}
int tested=0;for(var water:p.geysers){
level.getChunk(water.getX()>>4,water.getZ()>>4);
if(!level.getBlockState(water.below()).is(Blocks.POTENT_SULFUR)||!level.getBlockState(water.below(2)).is(Blocks.MAGMA_BLOCK)||level.getFluidState(water).isEmpty())throw new IllegalStateException("Invalid geothermal source "+water);
var state=level.getBlockState(water.below());
if(!(level.getBlockEntity(water.below()) instanceof net.minecraft.world.level.block.entity.PotentSulfurBlockEntity)
||((PotentSulfurBlock)Blocks.POTENT_SULFUR).getTicker(level,state,net.minecraft.world.level.block.entity.BlockEntityTypes.POTENT_SULFUR)==null)throw new IllegalStateException("Inactive geyser entity "+water);
if(++tested==8)break;
}
if(p.spikes<8||tips<8||tested<2)throw new IllegalStateException("Missing sulfur details "+p.spikes+" / "+tested);
return Map.of("spires",p.spikes,"nativeSpikeBlocksChecked",tips,"geysers",p.geysers.size(),"sourcesChecked",tested,"visits",p.geysers.stream().limit(8).map(BlockPos::toShortString).toList());
}
}
@@ -0,0 +1,149 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Bounded filling of open-sky depressions, with a full 3D escape test before retaining any water. */
public final class SurfaceWater187 {
record Pool(BlockPos seed,int surface,Set<BlockPos> cells,int area,int openSky) {}
record Plan(List<Pool> pools,Map<ChunkPos,Set<BlockPos>> chunks,Map<Long,Integer> surface,int probes,long millis) {}
private record Candidate(BlockPos floor,int score) {}
private static final Map<AdventureBiomes180,Plan> PLANS=Collections.synchronizedMap(new WeakHashMap<>());
private SurfaceWater187() {}
static Plan plan(AdventureBiomes180 s){synchronized(PLANS){return PLANS.computeIfAbsent(s,SurfaceWater187::find);}}
private static long column(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
static int surfaceAt(AdventureBiomes180 s,int x,int z){return plan(s).surface.getOrDefault(column(x,z),-1);}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
private record Range(int low,int high) {}
private record Avoid(Map<Long,Range> dungeon,List<Basins180.Basin> basins,Map<Long,Range> retainedWater) {}
private static void reserve(Map<Long,Range> m,int x,int z,int rx,int rz,int low,int high){
for(int dx=-rx;dx<=rx;dx++)for(int dz=-rz;dz<=rz;dz++)
m.merge(column(x+dx,z+dz),new Range(low,high),(a,b)->new Range(Math.min(a.low,b.low),Math.max(a.high,b.high)));
}
private static Avoid avoid(AdventureBiomes180 s){
var p=Dungeon180.plan(s);var m=new HashMap<Long,Range>();
for(var r:p.rooms())reserve(m,r.x(),r.z(),r.rx()+3,r.rz()+3,r.y()-12,r.y()+12);
for(var c:p.paths().values())reserve(m,c.x(),c.z(),3,3,c.y()-2,c.y()+6);
if(p.hut()!=null)reserve(m,p.hut().getX(),p.hut().getZ(),7,7,p.hut().getY()-9,p.hut().getY()+9);
var retained=new HashMap<Long,Range>();
if(s.field().coherent())for(var pool:NaturalWater184.plan(s).pools()){
var floor=new HashMap<Long,Integer>();
for(var cell:pool.cells())floor.merge(column(cell.getX(),cell.getZ()),cell.getY(),Math::min);
floor.forEach((key,low)->{
int x=(int)(key>>32),z=(int)(long)key;
reserve(retained,x,z,1,1,low-2,pool.surface()+2);
});
}
return new Avoid(Map.copyOf(m),Basins180.sites(s),Map.copyOf(retained));
}
private static boolean reserved(Avoid a,int x,int y,int z){
if(x*x+z*z<62*62)return true;
var occupied=a.retainedWater.get(column(x,z));if(occupied!=null&&y>=occupied.low&&y<=occupied.high)return true;
var r=a.dungeon.get(column(x,z));if(r!=null&&y>=r.low&&y<=r.high)return true;
for(var b:a.basins)if(Math.abs(y-b.y())<16&&Math.hypot(b.x()-x,b.z()-z)<76)return true;
return false;
}
private static Plan find(AdventureBiomes180 s){
long start=System.nanoTime();var avoid=avoid(s);var candidates=new ArrayList<Candidate>();
double spread=s.field().base().spread();int step=(int)Math.round(8*spread),extent=36*step;
for(int x=-extent;x<=extent;x+=step)for(int z=-extent;z<=extent;z+=step){
if(Math.hypot(x,z)>306*spread)continue;
int y=s.field().surface(x,z);if(y<170||y>290||reserved(avoid,x,y,z))continue;
int score=0;
for(var d:Direction.Plane.HORIZONTAL)for(int distance:new int[]{8,16,24})
if(s.field().surface(x+d.getStepX()*distance,z+d.getStepZ()*distance)>y+3)score++;
if(score>=5)candidates.add(new Candidate(new BlockPos(x,y,z),score));
}
candidates.sort(Comparator.comparingInt(Candidate::score).reversed());
var pools=new ArrayList<Pool>();int probes=0;
for(var candidate:candidates){
if(probes>=240||pools.size()==3)break;
var at=candidate.floor;
if(pools.stream().anyMatch(p->Math.hypot(p.seed.getX()-at.getX(),p.seed.getZ()-at.getZ())<120))continue;
probes++;
var best=flood(s,avoid,at.above(),at.getY()+16);
if(best!=null && (!s.field().coherent()||pools.stream().noneMatch(p->Continuity193.touches(p.cells(),best.cells()))))pools.add(best);
}
var chunks=new HashMap<ChunkPos,Set<BlockPos>>();var surface=new HashMap<Long,Integer>();
for(var pool:pools)for(var p:pool.cells){
chunks.computeIfAbsent(new ChunkPos(p.getX()>>4,p.getZ()>>4),key->new HashSet<>()).add(p);
if(p.getY()==pool.surface)surface.put(column(p.getX(),p.getZ()),p.getY());
}
chunks.replaceAll((k,v)->Set.copyOf(v));
return new Plan(List.copyOf(pools),Map.copyOf(chunks),Map.copyOf(surface),probes,(System.nanoTime()-start)/1_000_000);
}
private record Cell(BlockPos pos,int level) {}
private static Pool flood(AdventureBiomes180 s,Avoid avoid,BlockPos seed,int ceiling){
// Minimax flood: each voxel is sampled once, at the lowest water level which can reach it.
// Raising the water does not rescan the entire connected cavity sixteen times.
var queue=new PriorityQueue<Cell>(Comparator.comparingInt(Cell::level));
var seen=new HashSet<BlockPos>();var wet=new HashSet<BlockPos>();
var areas=new HashMap<Integer,Integer>();var skies=new HashMap<Integer,Integer>();
queue.add(new Cell(seed,seed.getY()));seen.add(seed);
int level=seed.getY(),lowest=level;Pool best=null;
while(!queue.isEmpty()){
var cell=queue.remove();var p=cell.pos;
if(cell.level>level){
int area=areas.getOrDefault(level,0),sky=skies.getOrDefault(level,0);
if(area>=180&&wet.size()>=400&&sky>=area*.65)best=new Pool(seed,level,Set.copyOf(wet),area,sky);
level=cell.level;
if(lowest<level-28)return best;
}
if(rock(s,p.getX(),p.getY(),p.getZ()))continue;
// Escaping a finite search box rejects this level. No generated dam or bed hides a leak.
if(Math.abs(p.getX()-seed.getX())>=72||Math.abs(p.getZ()-seed.getZ())>=72||p.getY()<level-28||wet.size()>=26000)return best;
if(reserved(avoid,p.getX(),p.getY(),p.getZ()))return best;
wet.add(p);lowest=Math.min(lowest,p.getY());
areas.merge(p.getY(),1,Integer::sum);
if(s.field().surface(p.getX(),p.getZ())<p.getY())skies.merge(p.getY(),1,Integer::sum);
for(var d:Direction.values()){
var q=p.relative(d);int next=Math.max(level,q.getY());
if(next<=ceiling&&seen.add(q))queue.add(new Cell(q,next));
}
}
int area=areas.getOrDefault(level,0),sky=skies.getOrDefault(level,0);
if(area>=180&&wet.size()>=400&&sky>=area*.65)best=new Pool(seed,level,Set.copyOf(wet),area,sky);
return best;
}
static void place(AdventureBiomes180 s,ChunkAccess chunk){
var cells=plan(s).chunks.get(chunk.getPos());if(cells==null)return;
for(var at:cells)if(chunk.getBlockState(at).isAir()){
chunk.setBlockState(at,Blocks.WATER.defaultBlockState());chunk.markPosForPostProcessing(at);
}
}
public static boolean protects(WorldGenLevel level,ChunkGenerator g,BlockPos pos){
if(!WorldgenLab.natural187(g)||pos.getY()<140||pos.getY()>310||Math.abs(pos.getX())>360||Math.abs(pos.getZ())>360)return false;
var cells=plan(Anchors187.source(g)).chunks.get(new ChunkPos(pos.getX()>>4,pos.getZ()>>4));
return cells!=null&&cells.contains(pos);
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var plan=plan(s);var details=new ArrayList<Map<String,Object>>();
for(var cp:plan.chunks.keySet())level.getChunk(cp.x(),cp.z());
for(var pool:plan.pools){
int water=0,leaks=0,sky=0;
for(var p:pool.cells){
if(rock(s,p.getX(),p.getY(),p.getZ()))throw new IllegalStateException("Surface water carved natural rock "+p);
if(!level.getBlockState(p).is(Blocks.WATER))continue;water++;
if(p.getY()==pool.surface){
boolean open=true;
// Ignore a rare floating reef high overhead; check the actual island canopy.
for(int y=p.getY()+1;y<320;y++)if(!level.getBlockState(new BlockPos(p.getX(),y,p.getZ())).isAir()){open=false;break;}
if(open)sky++;
}
for(var d:Direction.values())if(d!=Direction.UP){
var side=p.relative(d);var b=level.getBlockState(side);
if(!pool.cells.contains(side)&&b.getCollisionShape(level,side).isEmpty()&&b.getFluidState().isEmpty())leaks++;
}
}
if(water<400||leaks!=0||sky<pool.area*.60)throw new IllegalStateException("Open surface lake failure water="+water+" leaks="+leaks+" open="+sky+"/"+pool.area+" at "+pool.seed);
details.add(Map.of("seed",pool.seed.toShortString(),"surfaceY",pool.surface,"surfaceArea",pool.area,"openSkyArea",sky,"waterBlocks",water,"leaks",leaks));
}
if(details.isEmpty())throw new IllegalStateException("No open-sky surface basin; probes="+plan.probes+" planningMs="+plan.millis);
return Map.of("pools",details,"planningMs",plan.millis,"boundedFloodAttempts",plan.probes,"carvedRock",0,"artificialBed",0);
}
}
@@ -0,0 +1,59 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Exposed beds follow escarpments and summit shoulders, in the existing decoration pass. */
public final class Terrain189 {
static boolean isExposed(AdventureBiomes180 s,int x,int z){
if(s.originalRelief())return false;
if(s.field().refined())return Terrain190.isExposed(s,x,z);
int y=s.field().surface(x,z);if(y<252||y>316)return false;
for(var cherry:Cherry180.sites(s))if(Math.hypot(x-cherry.getX(),z-cherry.getZ())<14)return false;
int low=y;
for(int[] d:new int[][]{{8,0},{-8,0},{0,8},{0,-8}})low=Math.min(low,s.field().surface(x+d[0],z+d[1]));
return y-low>=7||y>=276&&EcologyBiomes176.noise(s.seed()^0x18957L,x/48.0,z/48.0)>.3;
}
public static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess c){
if(WorldgenLab.original191(g)||WorldgenLab.eroded192(g)||WorldgenLab.coherent193(g))return;
if(WorldgenLab.refined190(g)){Terrain190.place(level,g,c);return;}
if(!WorldgenLab.finishedTerrain(g))return;var s=Anchors189.source(g);
for(int x=c.getPos().getMinBlockX();x<=c.getPos().getMaxBlockX();x++)for(int z=c.getPos().getMinBlockZ();z<=c.getPos().getMaxBlockZ();z++){
if(!isExposed(s,x,z))continue;
int top=Anchors189.ground(s,x,z,s.field().surface(x,z));
for(int y=top;y>=top-3;y--){
var p=new BlockPos(x,y,z);if(!AdventureCaves180.natural(c.getBlockState(p)))continue;
Block b=Math.floorMod(y,24)<5?Blocks.TUFF:Math.floorMod(y,24)<9?Blocks.ANDESITE:Blocks.STONE;
c.setBlockState(p,b.defaultBlockState());
}
}
}
private static int preciseSummit(ReliefEcology177.Field field,int x,int z){
// The ordinary quart-column cache deliberately skips thin overhangs; a displacement test cannot.
for(int y=316;y>=252;y--)if(field.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0)return y;
return Math.min(251,field.surface(x,z));
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
if(s.field().eroded())return Erosion192.check(level,s);
if(s.originalRelief())return Original191.check(level,s);
if(s.field().refined())return Terrain190.check(level,s);
var field=s.field();var previous=new ReliefEcology177.Field(field.base(),field.peakX(),field.peakZ(),false);
int changed=0,exposed=0,deep=0,maxShift=0;
var heights=new TreeMap<Integer,Integer>();String largest="";
for(int x=-280;x<=280;x+=8)for(int z=-280;z<=280;z+=8){
int old=preciseSummit(previous,x,z),now=preciseSummit(field,x,z);if(old!=now){changed++;if(Math.abs(now-old)>maxShift){maxShift=Math.abs(now-old);largest=x+","+z+":"+old+" -> "+now;}}
if(now>252)heights.merge(now,1,Integer::sum);
if(isExposed(s,x,z))exposed++;
for(int y:new int[]{72,144,208,352,448}){
if(field.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)!=previous.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z))throw new IllegalStateException("Terracing changed caves or reefs");deep++;
}
}
if(changed<10||exposed<20||maxShift>12)throw new IllegalStateException("Missing or excessive terracing "+changed+" "+exposed+" "+maxShift+" at "+largest);
return Map.of("changedSurfaceSamples",changed,"exposedRockSamples",exposed,"unchangedDeepAndSkySamples",deep,"maxSurfaceShift",maxShift,"summitHeights",heights);
}
}
@@ -0,0 +1,61 @@
package fr.koka.sanctuarytest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Small organic shoulders and continuous outcrop masks; no quart-grid or regular height bands. */
final class Terrain190 {
private static final Map<AdventureBiomes180,Map<Long,Integer>> TOPS=Collections.synchronizedMap(new WeakHashMap<>());
private static int top(AdventureBiomes180 s,int x,int z){
Map<Long,Integer> cache;synchronized(TOPS){cache=TOPS.computeIfAbsent(s,k->new ConcurrentHashMap<>());}
return cache.computeIfAbsent(((long)x<<32)^(z&0xffffffffL),k->{
for(int y=316;y>=240;y--)if(s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0)return y;
return -1;
});
}
static boolean isExposed(AdventureBiomes180 s,int x,int z){
int y=top(s,x,z);if(y<252)return false;
for(var p:Cherry180.sites(s))if(Math.hypot(x-p.getX(),z-p.getZ())<14)return false;
double patch=EcologyBiomes176.noise(s.seed()^0x19057L,x/37.0,z/37.0);
int low=y;
for(var d:net.minecraft.core.Direction.Plane.HORIZONTAL)low=Math.min(low,Math.max(240,top(s,x+d.getStepX()*4,z+d.getStepZ()*4)));
// A few broad bare shoulders, plus steep exposed flanks; no four-block square boundary.
return y-low>5+3*patch || y>=272&&y-low>=2&&patch>.28;
}
static void place(WorldGenLevel level,ChunkGenerator g,ChunkAccess c){
var s=Anchors189.source(g);
for(int x=c.getPos().getMinBlockX();x<=c.getPos().getMaxBlockX();x++)for(int z=c.getPos().getMinBlockZ();z<=c.getPos().getMaxBlockZ();z++){
if(!isExposed(s,x,z))continue;
double bed=EcologyBiomes176.noise(s.seed()^0x190ADL,x/23.0,z/23.0);
Block material=bed>.32?Blocks.ANDESITE:bed<-.38?Blocks.TUFF:Blocks.STONE;
int y=top(s,x,z);
for(int h=0;h<3;h++){
var p=new BlockPos(x,y-h,z);
if(AdventureCaves180.natural(c.getBlockState(p)))c.setBlockState(p,material.defaultBlockState());
}
}
}
private static int summit(ReliefEcology177.Field f,int x,int z){
for(int y=316;y>=252;y--)if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0)return y;
return -1;
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var f=s.field();var original=new ReliefEcology177.Field(f.base(),f.peakX(),f.peakZ(),false);
int changed=0,total=0,max=0,deep=0,exposed=0;
for(int x=-280;x<=280;x+=8)for(int z=-280;z<=280;z+=8){
int old=summit(original,x,z),now=summit(f,x,z);if(old>=252){total++;if(old!=now){changed++;max=Math.max(max,Math.abs(old-now));}}
if(isExposed(s,x,z))exposed++;
for(int y:new int[]{72,144,208,248,352,448}){
if(f.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)!=original.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z))throw new IllegalStateException("Refinement touched deep or aerial field");deep++;
}
}
if(changed>total*.30||max>4||exposed<5)throw new IllegalStateException("Refinement too broad "+changed+"/"+total+" max="+max+" rocks="+exposed);
return Map.of("unchangedSummitSamples",total-changed,"localShoulderSamples",changed,"maxHeightShift",max,"exposedRockSamples",exposed,"unchangedDeepAndSkySamples",deep);
}
}
@@ -0,0 +1,78 @@
package fr.koka.sanctuarytest;
import java.util.*;
import net.minecraft.core.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** One upper lake and its descending spillway, chosen with bounded local density samples. */
final class Water183 {
record Site(int x,int y,int z,int dx,int dz,int score) {}
private static final Map<AdventureBiomes180,Site> SITES=Collections.synchronizedMap(new WeakHashMap<>());
private Water183() {}
private static boolean rock(AdventureBiomes180 s,int x,int y,int z){return s.field().sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)>0;}
static Site site(AdventureBiomes180 s){synchronized(SITES){return SITES.computeIfAbsent(s,key->{
Site best=null;
for(int x=-256;x<=256;x+=32)for(int z=-256;z<=256;z+=32){
double radius=Math.hypot(x,z);if(radius<196||radius>272||Dungeon180.near(s,x,z,80))continue;
boolean near=false;for(var b:Basins180.sites(s))if(Math.hypot(b.x()-x,b.z()-z)<110)near=true;
if(near)continue;
if(Math.hypot(x+152,z-152)<100)continue;
int dx=Math.abs(x)>=Math.abs(z)?Integer.signum(x):0,dz=dx==0?Integer.signum(z):0;
int top=s.terrain(x,z).top();
for(int y=Math.min(top,248);y>=196;y-=4){
if(!rock(s,x,y-4,z)||rock(s,x,y+4,z))continue;
int score=0,bed=0;
for(int u=-16;u<=48;u+=8)for(int v=-12;v<=12;v+=6){
int bx=x+dx*u-dz*v,bz=z+dz*u+dx*v,h=y-(u>24?4:0);
if(rock(s,bx,h-5,bz)){score+=3;bed++;}
if(!rock(s,bx,h+5,bz))score++;
}
if(bed<26)continue;
int ex=x+dx*48,ez=z+dz*48;
for(int h=y+4;h<=y+32;h+=4)if(!rock(s,ex,h,ez))score+=2;
if(best==null||score>best.score)best=new Site(x,y+1,z,dx,dz,score);
break;
}
}
if(best==null)throw new IllegalStateException("No supported upper spillway site");
return best;
});}}
static void place(AdventureBiomes180 s,ChunkAccess c){
var b=site(s);if(Math.abs(c.getPos().getMiddleBlockX()-b.x)>76||Math.abs(c.getPos().getMiddleBlockZ()-b.z)>76)return;
for(int x=c.getPos().getMinBlockX();x<=c.getPos().getMaxBlockX();x++)for(int z=c.getPos().getMinBlockZ();z<=c.getPos().getMaxBlockZ();z++){
double u=(x-b.x)*b.dx+(z-b.z)*b.dz,v=-(x-b.x)*b.dz+(z-b.z)*b.dx;
double wobble=.045*Math.sin(u*.23)*Math.cos(v*.31);
double upper=u*u/625+v*v/256+wobble,lower=(u-38)*(u-38)/361+v*v/144+wobble;
boolean channel=u>=19&&u<=40&&Math.abs(v)<=3;
double shape=Math.min(upper,lower);if(shape>1.12&&!channel)continue;
int waterY=b.y-(channel?Math.clamp((int)(u-20)/4,0,4):lower<upper?4:0);
boolean shore=shape>.88&&!channel;
int depth=shore?1:channel?2:2+(int)(2*Math.max(0,1-shape));
for(int y=waterY-depth-3;y<=waterY;y++){
Block material=y<waterY-depth?Blocks.TUFF:y==waterY-depth?Blocks.CLAY:shore?Blocks.MOSS_BLOCK:Blocks.WATER;
var p=new BlockPos(x,y,z);c.setBlockState(p,material.defaultBlockState());if(material==Blocks.WATER)c.markPosForPostProcessing(p);
}
int roof=b.y+6+(int)(8*Math.sqrt(Math.max(0,1-shape)));
// Downstream alcove opens to the daylight side while the upstream pool keeps its rock roof.
if(u>32)roof=Math.max(roof,s.terrain(x,z).top()+2);
for(int y=waterY+1;y<=roof;y++)c.setBlockState(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
}
}
static Map<String,Object> check(ServerLevel level,AdventureBiomes180 s){
var b=site(s);for(int x=(b.x-64)>>4;x<=(b.x+64)>>4;x++)for(int z=(b.z-64)>>4;z<=(b.z+64)>>4;z++)level.getChunk(x,z);
int connected=0;var seen=new HashSet<BlockPos>();var queue=new ArrayDeque<BlockPos>();queue.add(new BlockPos(b.x,b.y,b.z));
while(!queue.isEmpty()){
var p=queue.removeFirst();if(!seen.add(p)||Math.abs(p.getX()-b.x)>64||Math.abs(p.getZ()-b.z)>64||p.getY()<b.y-10||p.getY()>b.y)continue;
if(!level.getBlockState(p).is(Blocks.WATER))continue;connected++;
for(var d:Direction.values())queue.add(p.relative(d));
}
var lower=new BlockPos(b.x+38*b.dx,b.y-4,b.z+38*b.dz);
if(connected<1600||!seen.contains(lower))throw new IllegalStateException("Upper water disconnected: "+connected+" "+b);
var mouth=new BlockPos(b.x+45*b.dx,b.y-3,b.z+45*b.dz);
if(!level.canSeeSky(mouth))throw new IllegalStateException("Spillway has no exterior opening "+mouth);
return Map.of("upper",new BlockPos(b.x,b.y,b.z).toShortString(),"lower",lower.toShortString(),"connectedWaterBlocks",connected,"exteriorOpening",mouth.toShortString());
}
}

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