Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
454d9a5c08 | ||
|
|
ba1b228e76 | ||
|
|
1d60a3eedc | ||
|
|
16d81d892f | ||
|
|
7497d9182d | ||
|
|
6b97d57bde | ||
|
|
7c14696020 | ||
|
|
39ff7e95d5 | ||
|
|
918143aaa6 | ||
|
|
9da3f7f13b | ||
|
|
7179b5d4cb | ||
|
|
78f047d3dc | ||
|
|
c7f32c9416 | ||
|
|
97ea94e482 | ||
|
|
51e7e2590e | ||
|
|
658aaf8732 | ||
|
|
c53b39e71c | ||
|
|
f40382ad04 | ||
|
|
a067c7e66b | ||
|
|
6cfa4c07bf | ||
|
|
1ca01a47b7 | ||
|
|
a46946bd4c | ||
|
|
65e7127b1d |
@@ -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
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -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`.
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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é.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)&¤t.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
Reference in New Issue
Block a user