Compare commits

...
Author SHA1 Message Date
koka a49dfa5337 docs: record alpha30.3 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 13:34:14 +02:00
koka 076c9a6c16 feat: balance native plateaus and 3D mountains for alpha30.3
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 13:31:08 +02:00
koka 2a5b4cb51b docs: record alpha30.2 publication and preserved Prism instance
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 12:39:18 +02:00
koka c5381b939a feat(worldgen): irregular ledges and detached-rock filtering in alpha30.2
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 12:37:01 +02:00
koka a20d84811f docs: record verified alpha30.1 publication and Prism sync
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 11:48:38 +02:00
koka cccb89150e fix(worldgen): restore curved rivers and global relief in alpha30.1
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 11:47:00 +02:00
koka 071a8c062d Record verified alpha.30 publication and Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 10:38:52 +02:00
koka bf327b75d4 Deliver alpha.30 plateaus, open river banks and underground routes
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 10:36:55 +02:00
koka 8c90c61c53 Checkpoint alpha29 terrain ledges, transit and native mineshaft corrections 2026-09-12 09:44:30 +02:00
55 changed files with 2864 additions and 87 deletions
+43
View File
@@ -1,5 +1,48 @@
# Changelog
## 0.1.0-alpha.30.3
- Retire les microcorniches de la 30.2, jugées trop bruitées en jeu.
- Répartit de grandes zones de plateaux naturels et de montagnes déformées en 3D,
avec une échelle relative commune à Petit, Moyen et Grand.
- Étend les hauts reliefs vers Y360 sans changer la hauteur du monde ; conserve
les profondeurs, larrivée, les cours deau courbes et les infrastructures.
- Conserve un filtrage des fragments rocheux détachés jusque dans les sommets.
- Adapte la recherche aérienne aux plateaux disponibles : rejet grossier des
emprises trop creuses, contrôle de hauteur précoce et recherche autour de l’île.
- Corrige le veto de biome qui empêchait un sanctuaire minéral pourtant admis
de se poser dans les marais souterrains.
- Nouveau monde nécessaire pour tester le nouveau terrain ; aucun monde régénéré.
## 0.1.0-alpha.30.2
- Corniches locales dont la phase et lespacement suivent un bruit 3D, sur le relief global conservé.
- Filtrage borné des petits composants rocheux détachés dans la zone retouchée, indépendant des frontières de chunks.
- Rivières courbes, tunnels, sanctuaire minéral et lieux aériens conservés ; nouveau monde pour lessai.
- Minecraft 26.3-pre-2 et les dépendances restent inchangés. Voir `docs/testing-alpha30.2.md` pour les validations effectivement réalisées.
## 0.1.0-alpha.30.1 — 2026-09-12
Retrait des rivières sur grille de la 30 ; brèches de cascade sur les anciennes
rivières courbes et leurs bassins. Retour du relief global de la 28, sauf une
bande étroite de plateaux naturels pour les accès aux traversées.
Voir [le correctif](docs/generation-alpha30.1.md). Nouveau monde nécessaire.
## 0.1.0-alpha.30 — 2026-09-12
Trois zones de déformation 3D, plateaux naturels entre les massifs, grande rivière
à niveaux locaux et brèches de berge de cinq blocs maximum laissant couler leau.
Recherche des traversées mieux répartie, sanctuaire minéral avec seconde recherche
sur les flancs, mines vanilla mieux admises. Les navires conservent leur modèle
et leur placement proche du rivage. Nouveau monde de test nécessaire.
Voir [la génération](docs/generation-alpha30.md) et [les essais](docs/testing-alpha30.md).
## 0.1.0-alpha.29 — 2026-09-12
Petites corniches variables sur le relief 3D de la28 ; correction des traversées
et de ladmission des mines natives. Voir [le périmètre](docs/generation-alpha29.md)
et [les essais](docs/testing-alpha29.md). Nouveau monde de test nécessaire.
## 0.1.0-alpha.28 — 2026-09-12
Spéléologie par bruits natifs, déformations volumiques 3D et failles obliques bornées ;
+39 -2
View File
@@ -21,6 +21,43 @@ le relief et les souterrains de la 24, retire la montgolfière et travaille les
marais souterrains et la restauration de vrais modèles d’épaves vanilla. Voir le
[chantier et ses limites](docs/generation-alpha27.md).
## Alpha.30.3 — plateaux et grands reliefs
La passe de petites corniches de la 30.2 est retirée après le retour en jeu.
De larges provinces de relief font cohabiter les plateaux naturels issus de la
24 et les montagnes déformées en 3D, jusqu’à proximité de Y360. Leur répartition
est proportionnée aux trois tailles. Les petits fragments détachés restent filtrés.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.3.md)
et [les vérifications](docs/testing-alpha30.3.md).
## Alpha.30.1 — rivières courbes et relief global
Les rivières angulaires de la 30 sont retirées. Les anciens cours deau et
bassins peuvent déborder par une courte brèche dans leur berge. Le relief 3D
redevient global, avec une seule bande étroite de plateaux naturels pour
faciliter les accès aux traversées.
Créer un **nouveau monde**. Voir [le correctif](docs/generation-alpha30.1.md)
et [ses vérifications](docs/testing-alpha30.1.md).
## Alpha.30 — plateaux, rivières ouvertes et traversées
La déformation 3D se concentre dans trois grandes zones ; les plateaux naturels
reprennent leur place entre elles. Une rivière cherche un parcours à l’échelle
de l’île. Des brèches courtes dans ses berges et celles des bassins laissent
leau tomber sur les flancs ou dans le vide. Les traversées souterraines et
le placement du sanctuaire minéral sont renforcés ; les navires restaurés
restent proches des rivages.
Créer un **nouveau monde**. Voir [la génération](docs/generation-alpha30.md),
[les essais et leurs limites](docs/testing-alpha30.md) et [packwiz](docs/packwiz.md).
Lalpha.29 reste un point de reprise local non publié, intégré à cette livraison.
Le [MRpack alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30/Sanctuary-0.1.0-alpha.30.mrpack)
est publié ; le canal packwiz et linstance Prism existante sur Mac sont
synchronisés. Les essais natifs et le rechargement sont validés sur Mac.
## Alpha.28 — grottes et lieux proches
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
@@ -585,7 +622,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
| Fabric API | 0.160.0+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | 0.1.0-alpha.28 |
| Sanctuary / pack | 0.1.0-alpha.29 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -614,7 +651,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.28.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.29.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+49
View File
@@ -978,3 +978,52 @@ Moins de marais ; mines, huttes et donjons vanilla adaptés aux cavités réelle
Les filons natifs doivent aussi alimenter les continents ouverts ensuite.
Aucun retour des salles ellipsoïdales ni des cônes de lalpha.25.
Résultats et limites : [génération](generation-alpha28.md), [essais](testing-alpha28.md).
### WG-31 — Corniches fines et traversées présentes — alpha.29
Branche `codex/terraces-transit-alpha29`, depuis lalpha.28 validée en jeu
sur Windows par le créateur. Reproduire la traversée manquante sur Grand,
graine `-7228211907433324401`, corriger ladmission et la pose des modules.
Ajouter un léger terracement variable au relief3D, préserver les profondeurs,
et vérifier les véritables mines vanilla sur la graine signalée.
Voir [génération](generation-alpha29.md) et [vérifications](testing-alpha29.md).
### WG-32 — Dernière passe de terrain alpha — alpha.30
Branche `codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61`. Concentrer la déformation signée dans trois zones, retrouver les
plateaux naturels entre elles et des rivières longues à niveaux locaux.
Ouvrir des brèches de berge courtes pour laisser leau couler librement.
Assurer une recherche mieux répartie des traversées, notamment en Grand ;
chercher aussi le sanctuaire minéral sur les flancs en cas de refus intérieur.
Conserver les bateaux proches, les cavités et les générateurs vanilla de la 28/29.
Voir [le périmètre](generation-alpha30.md) et [les preuves](testing-alpha30.md).
### WG-33 — Rivières courbes et relief global — alpha.30.1
Branche `codex/curved-rivers-alpha30-1`. Retirer la rivière angulaire de la 30,
retrouver la déformation globale de la 28 avec une zone restreinte de plateau,
et ouvrir les berges des anciens cours deau vers les espaces vides proches.
Conserver les recherches de traversées et de sanctuaire améliorées en 30.
Voir [le correctif](generation-alpha30.1.md) et [les essais](testing-alpha30.1.md).
### WG-34 — Corniches irrégulières et fragments — alpha.30.2
Branche `codex/irregular-relief-alpha30-2`. Conserver les grandes masses et
arches, casser la régularité des pentes par des corniches localisées, et retirer
les petits fragments détachés issus du champ de terrain retouché. Comparer les
mêmes volumes natifs avant/après ; contrôler les raccords aux frontières de chunks.
Voir [la génération](generation-alpha30.2.md) et [les essais](testing-alpha30.2.md).
### WG-35 — Plateaux naturels et grandes montagnes — alpha.30.3
Branche `codex/balanced-relief-alpha30-3`. Le rendu de la 30.2 est rejeté en jeu :
les métriques locales de marches ne constituaient pas une validation esthétique.
Retirer ce microrelief, retrouver des plateaux issus de la 24 et réserver une
part importante aux grandes déformations 3D, vers Y360. Équilibrer les trois
tailles par une distribution à l’échelle de l’île. Préserver les profondeurs,
les cours deau et les infrastructures ; limiter les petits fragments flottants.
Voir [le changement](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md).
+56
View File
@@ -0,0 +1,56 @@
# Alpha.29 — corniches fines et traversées présentes
La 28 a été validée visuellement par le créateur sur Windows, avec un chargement
correct. Cette livraison conserve cette base et traite trois observations :
la grande traversée absente en Grand, le relief un peu trop lissé et les mines
vanilla difficiles à rencontrer.
## Témoin signalé
Grand **1 024**, graine **-7228211907433324401**. La reproduction avec le code
28 confirme un plan de transit vide : 419 alignements examinés, 13 000 colonnes,
75,114 secondes de recherche et aucun couple de portiques naturels accepté.
Dans les douze premiers candidats natifs examinés, aucune mine nest admise.
Le résultat ne prétend pas inventorier les parties inconnues de toute l’île.
## Relief
La déformation 3D de la 28 reste en place. Une modulation fine du Y échantillonné
forme de petites corniches avec des ruptures plus franches. Période locale de
6 à 9 blocs et phase variables avec les bruits 3D existants, décalage supplémentaire
borné à ±2,5 blocs. Aucune grille de plateaux horizontaux commune à toute l’île.
Le raccord commence à Y214 et atteint sa pleine amplitude à Y244, à l’écart
de larrivée et du contour. Les densités sous Y214 sont exactement celles de
la 28 ; les grottes, failles, minerais, marais et lieux aériens sont conservés.
Le même champ sert aux colonnes, chunks, biomes et certifications.
## Infrastructures et mines
Le correctif de traversée conserve le génie civil et les galeries industrielles
existants. Les accès doivent sadapter au relief sans transformer la surface en
une grande fondation. Deux accès relient une chaussée et ses trottoirs ; la
branche vers une salle conserve une paroi pleine du côté fermé de sa jonction.
Les modules déjà certifiés ne sont plus refiltrés par un
biome souterrain incompatible avec leur ancien tag de structure.
Pour les mines, les protections sont vérifiées autour des vraies pièces et de
leurs appuis, et non dans tout le rectangle vide entre leurs branches. Le
graphe, les pièces, les rails, coffres, toiles et spawners restent vanilla.
Un sol rocheux portant déjà la galerie est reconnu comme appui natif ; il ne
nécessite pas un second pilier ou une chaîne. Les galeries exposées gardent les
ancrages bornés du vanilla et les réservations protègent aussi leurs appuis.
Les trois essais daltitude sont choisis dans les couches rocheuses réellement
présentes autour des pièces, après un sondage de 32 colonnes au maximum. Les
croisements utilisent le contrat de leur plancher vanilla : une voûte rocheuse
proche nest pas nécessaire à un pont de planches portant sur un corridor ancré.
Les marches creusées et les salles gardent leurs contrôles de couverture.
Les mines restent conditionnelles : le correctif nimpose pas une mine à chaque
graine et ne modifie pas la politique des autres familles de structures.
## Distribution
Mod et pack **0.1.0-alpha.29**, Minecraft **26.3-pre-2**, codec de prototype
`sanctuary:island_v24`, mêmes tailles et bouton Personnaliser. Les options de
structures gardent leur autorité. Créer un nouveau monde ; aucune modification
ou régénération dune sauvegarde personnelle. Les artefacts28 restent accessibles.
Les constats mesurés et les limites sont consignés dans [les essais](testing-alpha29.md).
+46
View File
@@ -0,0 +1,46 @@
# Alpha.30.1 — retour aux rivières courbes et au relief global
Correctif demandé après lessai de la 30 : les nouvelles rivières angulaires
évoquaient des canaux construits, et la limitation du relief à trois secteurs
rendait trop de surface plate.
## Changements
- Suppression du générateur de rivière sur grille ajouté en 30 et de son plan
global. Lhydrologie retrouve exclusivement les rivières courbes, bassins,
terrasses et sources déjà présents dans `PopulationHydrology`.
- Retour de la déformation 3D signée de la 28 sur lensemble du terrain, sauf
une bande étroite de plateaux naturels vers Z = rayon/4. Elle suit un axe
dimplantation possible des accès de traversée. La transition est progressive ;
le champ antérieur y est retrouvé sans dalle ajoutée ni remplissage des grottes.
Les tests synthétiques gardent le poids global exact sur plus de 85 % de
lemprise circulaire, et un cœur de plateau sur moins de 8 %.
- Brèches recherchées dans les **berges des anciens cours deau**, avec priorité
aux rivières, puis aux lacs et petits bassins. Les berges sèches déjà garnies
de sédiments ne bloquent plus cette recherche, contrairement à la 30.
- Chaque brèche est longue de cinq blocs maximum et large de trois blocs.
Son fond descend légèrement pour laisser l’écoulement vanilla atteindre la
lèvre. Elle ne crée ni nouvelle source, ni rivière supplémentaire, ni
fondation sous la chute. Aucune vérification dun bassin de réception :
leau peut descendre sur l’île ou dans le vide.
- Jusqu’à six brèches espacées par région, uniquement lorsquune rive est
assez proche dun espace ouvert. Un bassin situé loin du bord reste fermé.
Les tracés courbes ne sont pas redessinés par la passe de brèche. Le retour du
relief global peut toutefois changer leur implantation dune version à lautre,
puisquils recherchent leur place dans la roche disponible.
Les corrections de traversées, le sanctuaire minéral, les mines vanilla,
les navires restaurés et les deux îlots restent présents. La 30.1 conserve
Minecraft **26.3-pre-2**, les tailles **512 / 724 / 1 024**, Moyen par défaut,
et le bouton natif Personnaliser. Aucune nouvelle interface à traduire.
## Essai et distribution
Version mod et pack : **0.1.0-alpha.30.1**, branche
`codex/curved-rivers-alpha30-1`. **Créer un nouveau monde** : le prototype
conserve `sanctuary:island_v24` ; aucune migration ou régénération de sauvegarde
personnelle nest effectuée. Les structures conservent leurs options existantes.
Résultats effectivement vérifiés et coordonnées dans
[les essais](testing-alpha30.1.md). La distribution suit [packwiz](packwiz.md).
+50
View File
@@ -0,0 +1,50 @@
# Alpha.30.2 — corniches irrégulières et petits fragments
Retour visuel du 12 septembre 2026 : conserver les grandes masses et les arches
acceptées, mais réduire leurs contours trop réguliers et les longues pentes
en marches répétées. Le rendu des captures sert de contre-exemple ; il ne
sagit pas de remplacer les massifs par de nouvelles formes prédéfinies.
## Passe locale
Le relief global de la 28/30.1 reste actif, ainsi que sa zone limitée de plateau.
Une adaptation locale du point d’échantillonnage vertical crée de courtes
corniches : leur phase et leur espacement suivent un bruit 3D existant, à
l’échelle de quelques dizaines de blocs. Les couches ne partagent pas un même
niveau horizontal à travers toute l’île. Le déplacement supplémentaire du
point d’échantillonnage reste borné à **2,25 blocs**.
La passe satténue sous Y188 et disparaît sous Y140, près de larrivée, dans le
cœur de la bande de plateau, à lextrémité du contour et au-dessus du terrain.
Les volumes profonds et les paramètres des grottes natives ne sont pas changés.
Les rivières courbes, sorties de berge et leurs écoulements gardent leur
algorithme. Comme pour toute modification de densité, la recherche dun lieu
compatible peut aboutir à des coordonnées différentes sur une même graine.
## Petits blocs flottants
Une recherche de connexion dans le **champ naturel avant décoration** retire
les composants détachés de **32 blocs au maximum** dans la zone retouchée.
Elle sarrête dès quelle rejoint 33 blocs connectés ou la roche conservée hors
retouche. Les six faces servent de voisinage ; les diagonales ne suffisent pas.
Les coordonnées sont globales : les bords de chunks ne coupent pas une attache.
Ce contrôle ne remplit aucun trou, ne rajoute aucun pilier, ne visite aucune
sauvegarde personnelle et ne retire pas les îlots aériens. Il précède la pose
ultérieure des arbres, structures et fluides. Il ne constitue pas une promesse
quaucun élément flottant de quelque origine que ce soit ne puisse exister.
Le cache est borné, propre au thread et invalidé selon le sampler et son contexte,
avec des références faibles aux identités de monde.
## Livraison
Version mod et pack **0.1.0-alpha.30.2**, branche
`codex/irregular-relief-alpha30-2`. Nouveau champ de codec optionnel
`surface_302`, faux lorsquil est absent. Activé dans les trois profils actuels
512 / 724 / 1 024 ; Moyen reste le choix par défaut. Le prototype garde
`sanctuary:island_v24`, Minecraft 26.3-pre-2 et le bouton natif Personnaliser.
**Créer un nouveau monde pour essayer le rendu** ; aucune régénération des
sauvegardes personnelles. Pas de nouvelle interface nécessitant une traduction.
Les vérifications et limites sont dans [les essais](testing-alpha30.2.md).
La publication et la synchronisation suivent [packwiz](packwiz.md).
+59
View File
@@ -0,0 +1,59 @@
# Alpha.30.3 — plateaux et grands reliefs
## Choix retenu
Le créateur a rejeté la 30.2 après essai : la retouche fine produisait un aspect
rongé et trop bruité. Son nombre réduit de longues marches ne garantissait pas
un meilleur paysage. Cette version désactive `surface_302` et `terraces_29`.
La roche reste le champ volumétrique Minecraft de la base alpha.24, accompagné
des grottes et fractures adaptées en 28. Un bruit de grande longueur donde
sélectionne des provinces montagneuses ; il ne dessine ni cônes ni salles.
Les provinces sont échantillonnées dans un repère proportionnel au rayon :
Petit 512, Moyen 724 et Grand 1 024 conservent une distribution comparable,
avec des masses physiquement plus vastes lorsque l’île grandit.
Le seuil médian du champ intérieur conserve une grande part des plateaux ;
la transition vers les provinces hautes est continue. La bande de plateau
utile aux infrastructures reste protégée, ainsi que les 48 blocs autour du
centre. Aucun pavage périodique ou bruit supplémentaire à l’échelle du bloc.
Dans les provinces hautes, le domaine vertical s’étend progressivement
au-dessus de Y170, avec les déplacements signés sur trois axes de la 28.
Cela relève la roche existante et conserve sa structure volumétrique, ses
surplombs et ses vides. Une extinction douce sous Y360 laisse de la place
sous le plafond de construction Y384. La hauteur atteinte dépend du terrain
et de la graine ; aucun sommet nest forcé à une altitude identique.
Le filtre de connexité travaille sur le champ rocheux avant décoration :
composants de 32 blocs au plus, connexions par faces, coordonnées globales
même à cheval sur plusieurs chunks. Il ne crée aucun pilier ni roche de
remplissage. Les grandes masses flottantes naturelles et les îlots aériens
ne sont pas supprimés.
Les grottes sous Y170 conservent exactement leur densité précédente. Les
anciens cours deau courbes et leurs brèches de débordement, la traversée,
le sanctuaire minéral, les secrets et les quatre lieux aériens sont conservés.
Leurs recherches utilisent le terrain courant ; leurs emplacements peuvent
changer. Pas de retour de la montgolfière ou des rivières angulaires.
Le biome `sanctuary:swamp27_caves` rejoint les biomes autorisés des réseaux
secrets : un sanctuaire minéral déjà certifié ne doit plus être annulé au
stade du départ natif simplement parce quil se trouve dans ce marais.
La recherche des îlots refuse immédiatement les volumes trop hauts, dès le
premier obstacle incompatible, et filtre grossièrement les emprises au-dessus
de trop grands trous avant leur certification bloc par bloc. Après le secteur initial, les candidats parcourent tout le tour de l’île.
La recherche reste bornée à 64 candidats par site et 16 384 colonnes au total ;
la certification détaillée des sites acceptés est conservée.
## Essai
Minecraft 26.3-pre-2, mêmes dépendances et bouton natif Personnaliser.
Créer un **nouveau monde Sanctuary**. Le codec reste `sanctuary:island_v24`,
conformément au workflow de mondes de test jetables demandé par le créateur.
Les ressources des trois profils sont mises à jour : ne pas prolonger une
ancienne sauvegarde pour comparer des raccords. Cette livraison nouvre et
ne régénère aucune sauvegarde personnelle.
Les prévisualisations de densité servent à examiner les silhouettes ; elles
ne remplacent pas lessai visuel du paysage décoré dans le client Minecraft.
Voir [les validations réalisées](testing-alpha30.3.md).
+80
View File
@@ -0,0 +1,80 @@
# Alpha.30 — plateaux, rivières et traversées
Dernière passe de génération alpha demandée par le créateur. Branche
`codex/terrain-finale-alpha30`, depuis le point de reprise local alpha.29
`8c90c61` ; cette dernière na pas été publiée. Minecraft **26.3-pre-2**,
Fabric Loader **0.19.5**, Fabric API **0.160.0+26.3** conservés.
## Relief
La déformation signée par les bruits Minecraft de la 28 agit dans trois zones
étendues, orientées par la graine. Entre elles, son poids devient nul et le
champ de plateaux naturel précédent reprend sa place. Il sagit de déplacer
et daffaisser la roche existante, pas dajouter trois cônes. Le petit
terracement systématique essayé en 29 est désactivé.
Les cavités et failles bornées de la 28 restent actives. Aucun nouveau
creusement ellipsoïdal, plafond du monde ou changement des nuages. La variation
de relief est nulle sous Y180. Les tailles 512 / 724 / 1 024 et le bouton natif
Personnaliser sont conservés, avec Moyen par défaut.
Ce changement vise des étendues plus calmes séparées par du relief marqué.
Il ne garantit pas que toute surface ait exactement deux niveaux, ni que toute
petite pointe rocheuse ait disparu. Le rendu est à juger dans un nouveau monde.
## Hydrologie
Une recherche sur la principale masse connectée cherche une rivière longue,
évitant les fortes pentes quand un passage plus bas existe. Elle suit le terrain
réel avec un lit de cinq blocs environ, des niveaux locaux, du gravier et de
largile. Les petits bassins et réseaux précédents restent présents.
Quand une berge est séparée dun espace ouvert par **cinq blocs au maximum**,
une brèche de trois blocs de largeur ouvre le passage. Le plan cherche jusqu’à
six sorties espacées pour la grande rivière et une sortie supplémentaire par
réseau régional. Il nimpose pas deux cascades terminales, ni de bassin de
réception : leau peut descendre sur l’île ou dans le vide. La brèche ne pose
ni fondation ni colonne deau artificielle ; les ticks vanilla propagent leau
depuis les sources voisines. Les emprises de chute restent réservées aux
expansions. Larrivée naturelle reste à l’écart du tracé.
La recherche travaille sur une grille puis vérifie les blocs du lit. Les
passages trop creux peuvent interrompre ce lit et laisser fuir leau. Ce nest
pas une simulation de bassin versant avec débit conservé ou pente monotone.
Ces ajouts concernent l’île initiale ; les profils des continents dexpansion
ne sont pas remplacés par cette rivière.
## Traversée et sanctuaire minéral
La recherche des deux entrées répartit plus tôt ses essais entre plusieurs
alignements au lieu d’épuiser une seule bande. Les appuis et la protection
des secrets gardent leur autorité. La seconde passe peut omettre un pied
optionnel protégé seulement si les autres appuis restent valides. Les modules
industriels, escaliers, trottoirs et parois existants sont conservés.
Le sanctuaire garde ses 79 × 73 × 27 blocs et sa palette minérale. Après les
48 candidats intérieurs, une seconde recherche de 48 candidats sur les flancs
accepte davantage dexposition, avec des attaches rocheuses et le plancher
praticable. Un échec complet reste diagnostiqué ; la structure ne peut pas être
posée dans un volume intégralement vide ou en écrasant un autre secret.
Aucun boss, machine, portail actif ou dimension ajouté.
Les corrections de mines de la 29 sont intégrées : hauteur choisie dans la
roche disponible, admission des pièces et appuis réels, croisements en bois
acceptés lorsquils sont portés. Les pièces, spawners, rails et butins restent
vanilla. Leur présence reste conditionnelle aux candidats et au terrain.
## Satellites et sauvegardes
Deux navires restaurés près des rivages et deux îlots au-dessus de Sanctuary,
dont un à cascade : modèles et règles de proximité conservés. Les points de
vue doivent désormais éviter les sommets que lhydrologie va creuser en bassin
ou en brèche ; aucun bloc artificiel nest ajouté pour soutenir un témoin.
Le prototype conserve `sanctuary:island_v24` et ses profils courants, conformément
au choix de travailler sur des mondes de test jetables. **Créer un nouveau monde.**
Il ny a ni migration ni régénération dune sauvegarde personnelle. Les réglages
Générer les structures et Structures expérimentales gardent leur rôle ; la
rivière et le relief sont des phénomènes naturels indépendants de ces options.
Les résultats effectivement mesurés sont dans [les vérifications](testing-alpha30.md).
+129
View File
@@ -13,6 +13,135 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## Alpha.30.3 publiée — 12 septembre 2026
La [release alpha.30.3](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.3)
est publiée depuis `076c9a6c16e93e46db4a16ed0dd8d6a5a22c5a85`, branche
`codex/balanced-relief-alpha30-3`. Elle retire les microcorniches de la 30.2,
mélange plateaux naturels et grandes provinces montagneuses en 3D et conserve
le filtrage des petits fragments. Les recherches aériennes et le veto de biome
du sanctuaire dans les marais sont corrigés.
`check build` et `assemblePack` réussissent. Les trois profils sont comparés
sur les graines 0, 42 et `-7228211907433324401`. Trois mondes natifs ont terminé
à 24/24 tests ; Petit 42 et Grand signalé comprennent les correctifs finaux de
placement. Voir [le terrain](generation-alpha30.3.md) et [les preuves](testing-alpha30.3.md).
Le [MRpack alpha.30.3](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.3/Sanctuary-0.1.0-alpha.30.3.mrpack),
le JAR et le ZIP damorçage ont été vérifiés localement et par téléchargement public.
Canal packwiz : `a68fe0df2f82868100219799db603805f89c56c7`.
Minecraft 26.3-pre-2, Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
Deux installations isolées puis deux synchronisations de la même instance
**Sanctuary Beta sur Mac**, jeu fermé, ont réussi. Un seul JAR Sanctuary 30.3
est actif ; les 925 fichiers personnels et réglages suivis gardent leurs empreintes.
Le dossier dinstance reste `Sanctuary-0.1.0-alpha.1`. Sauvegarde ciblée :
`sanctuary-backups/before-0.1.0-alpha.30.3/`.
SHA-256 JAR : `6713622a4ad8034bb230ff11e1bad580d218932da31b3feeddee4c905fd11759`.
SHA-256 MRpack : `bb1172d14a8092d8155edeb61dba943c0a149874580ab4bd21abd8e87db54255`.
Reçus : `build/alpha303-delivery-public.json`, `build/alpha303-isolated-validation.json`,
`build/alpha303-prism-validation.json`.
Créer un **nouveau monde Sanctuary**. Aucun essai natif Windows, mise à jour
Windows ou validation esthétique par le créateur ne sont revendiqués.
## Alpha.30.2 publiée — 12 septembre 2026
La [release alpha.30.2](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.2)
est publiée depuis **`c5381b939ad548511455f5251b51443dba4bf8d5`**, branche
`codex/irregular-relief-alpha30-2`. Elle conserve les grandes masses et les
arches, ajoute des corniches locales irrégulières et filtre les petits
composants rocheux détachés dans la zone retouchée. Voir
[la génération](generation-alpha30.2.md) et [les vérifications](testing-alpha30.2.md).
`check build` et `assemblePack` réussissent sur Mac. Les **24 GameTests** du
Moyen 0 et les **24 GameTests** du Grand `-7228211907433324401` passent ; les
volumes natifs, lordre d’échantillonnage, les cascades et les traversées sont
contrôlés. Le rendu complet avec végétation reste à apprécier en jeu.
Le [MRpack alpha.30.2](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.2/Sanctuary-0.1.0-alpha.30.2.mrpack),
le JAR et le ZIP damorçage sont vérifiés localement puis par téléchargement
public. Le canal packwiz est au commit
**`b9eac60a25d207540efa5a9f2d2d2087b41c86f4`**.
Minecraft 26.3-pre-2, Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 sont conservés.
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**
sur Mac, réussissent, jeu fermé. Un seul JAR Sanctuary alpha.30.2 est actif ;
les empreintes des **925 fichiers personnels et réglages suivis** sont conservées.
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30.2/`.
SHA-256 JAR : `a11a47f08b052703892fd9a705636863f0c9313e9101641538c1b6f37ff2873a`.
SHA-256 MRpack : `f80cd65e10b66851a13e21fa878101c198bec3e115c26676981dc1275c735bfa`.
Reçus : `build/alpha302-delivery-public.json`,
`build/alpha302-isolated-validation.json`, `build/alpha302-prism-validation.json`.
Créer un **nouveau monde**. Témoin simple : **Moyen 724, graine 0**.
Aucun essai Windows natif ni mise à jour de linstance Windows nest revendiqué.
## Alpha.30.1 publiée — 12 septembre 2026
La [release alpha.30.1](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30.1)
est publiée depuis **`cccb89150e7e7c7bb13713aeab579e9203ff0845`**, branche
`codex/curved-rivers-alpha30-1`. Elle retire les nouvelles rivières sur grille,
restaure le relief 3D global avec une bande de plateau, et ouvre de courtes
brèches dans les berges des anciens cours deau. Voir
[la génération](generation-alpha30.1.md) et [les preuves](testing-alpha30.1.md).
`check build` et `assemblePack` passent sur Mac. Les **24 GameTests** du témoin
Moyen 0, puis les **24 GameTests** du Grand `-7228211907433324401` après
rechargement, réussissent. L’écoulement des cascades est vérifié sous ticks
réels ; les contrôles ne remplacent pas lappréciation visuelle en jeu.
Le [MRpack alpha.30.1](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30.1/Sanctuary-0.1.0-alpha.30.1.mrpack),
le JAR et le ZIP damorçage sont vérifiés localement puis par téléchargement
public. Le canal packwiz est au commit
**`3d1680f9f2722304ee0ad5950cc71fa61987d2f3`**. Minecraft 26.3-pre-2,
Fabric Loader 0.19.5 et Fabric API 0.160.0+26.3 restent inchangés.
Deux synchronisations isolées, puis deux dans linstance existante
**Sanctuary Beta** sur Mac, réussissent, jeu fermé. Un seul JAR Sanctuary
alpha.30.1 est actif ; les empreintes des **925 fichiers personnels et réglages
suivis** sont conservées. Sauvegarde ciblée :
`sanctuary-backups/before-0.1.0-alpha.30.1/`.
SHA-256 JAR : `d719a2d7bb544299ecc4adba617ea524d898ec003df35cf3464877fb74a7d0dd`.
SHA-256 MRpack : `37c576d868071a078619b4fd53a1a1eff9cd99ff7dbb16c146bd7360bc4e9274`.
Reçus : `build/alpha301-delivery-public.json`,
`build/alpha301-isolated-validation.json`, `build/alpha301-prism-validation.json`.
Créer un **nouveau monde**. Essai conseillé : **Moyen 724, graine 0** ; sorties
de lancienne rivière vers **13 246 82** et **-105 246 -65**. Aucun essai Windows
natif de ce correctif ni modification de linstance Windows nest revendiqué.
## Alpha.30 publiée — 12 septembre 2026
La [release alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.30)
est publiée depuis **`bf327b7`**, branche `codex/terrain-finale-alpha30`.
Le point de reprise local alpha.29 est inclus sans publication séparée.
Voir [la génération](generation-alpha30.md) et [les preuves](testing-alpha30.md).
`check build`, les 24 GameTests natifs du témoin Grand42, son redémarrage et
`assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP damorçage sont
vérifiés localement puis par téléchargement public. Le
[MRpack alpha.30](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.30/Sanctuary-0.1.0-alpha.30.mrpack)
est disponible ; Minecraft 26.3-pre-2 et les dépendances restent inchangés.
Le canal packwiz est au commit **`77bc330da65f93e08e52ea9c81b73bb42f1fdd2b`**.
Deux synchronisations isolées puis deux dans linstance existante **Sanctuary Beta**
sur Mac réussissent, jeu fermé. Un seul JAR Sanctuary alpha.30 est actif ;
les empreintes des **925 fichiers personnels et réglages suivis** sont conservées.
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.30/`.
SHA-256 JAR : `ebd9e32a32a548985b0de95e1e850212a5bf03b50cfef59db143896312807d7a`.
SHA-256 MRpack : `f029912098c823bb5e0b7609388856d46c53e89b8172d295fd975269a0e43ce6`.
Reçus : `build/alpha30-delivery-public.json`,
`build/alpha30-isolated-validation.json`, `build/alpha30-prism-validation.json`.
Créer un **nouveau monde**. Les fichiers de linstance Windows nont pas été
modifiés ; aucune validation Windows native de cette version nest revendiquée.
La disposition des plateaux et les grandes cascades restent à apprécier en jeu.
## Alpha.28 publiée — 12 septembre 2026
La [release alpha.28](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.28)
+109
View File
@@ -0,0 +1,109 @@
# Alpha.29 — vérifications
**Point de reprise local, non publié.** Le chantier se poursuit directement
en alpha.30 à la demande du créateur. Le contrôle supplémentaire Petit512,
graine2026, a refusé la traversée et révélé un témoin côtier aérien sans appui
après génération. Ces limites empêchent de considérer la29 comme livrée.
Les artefacts préparatoires et reçus locaux restent des preuves intermédiaires.
État des preuves au 12 septembre 2026. Les essais natifs ci-dessous ont été
exécutés sur **Mac, en serveur de développement sans rendu client**. Le retour
Windows du créateur concerne lalpha.28 ; aucun essai Windows de la 29 nest
revendiqué. Créer un **nouveau monde** pour tester cette génération.
## Traversées réellement générées et parcourues
| Monde | Départs / chunks FULL | Distance entre accès | Parcours avec collisions natives |
|---|---:|---:|---:|
| Grand 1 024, graine **-7228211907433324401** | 23 / 52 | 340 blocs | 652 pas, 74 points accessibles |
| Moyen 724, graine **0** | 17 / 27 | 238 blocs | 390 pas, 56 points accessibles |
Chaque témoin vérifie les blocs posés, les escaliers, les approches naturelles,
les raccords entre modules, les départs sauvegardables et le locator dentrée.
Les **cinq cellules du côté fermé de la jonction en T** sont contrôlées après
la correction du renderer. Ces recherches nouvrent aucune expansion.
Preuves : `build/alpha29-evidence/user-locate-before-crossing/transit29-native.json`
(PID 55232) et `build/alpha29-evidence/medium-0/` (reçus du PID 56574).
La correction ultérieure de ladmission des mines ne modifie pas le transit.
Sur la même graine Grand, la référence alpha.28 produisait un plan vide après
419 alignements et 13 000 colonnes. Cette reproduction est conservée dans
`build/alpha29-evidence/baseline-user/` ; le succès de ses anciens GameTests
ne démontrait pas la présence dune traversée.
## Relief : comparaison exacte avec la 28
| Monde | Densités inférieures identiques bit à bit | Échantillons supérieurs ayant changé de signe |
|---|---:|---:|
| Grand, graine signalée | 10 140 | 303 / 71 656, soit **0,423 %** |
| Moyen, graine 0 | 7 935 | 166 / 56 074, soit **0,296 %** |
Les densités inférieures sont échantillonnées entre Y32 et Y214. Les deux
coupes à Z0 et Z96, au pas dun bloc dans la bande Y205305, ont été comparées
pour la graine signalée : petites corniches, quelques arêtes plus dentelées,
grandes silhouettes de la 28 conservées. Cela ne remplace pas un essai visuel
dans le client. Le déplacement supplémentaire des coordonnées est borné à
2,5 blocs ; cette borne ne porte pas sur le changement de sommet lorsquune
petite lèvre disparaît.
Reçus `alpha29-terraces-*.json` des dossiers de preuve ; aperçu local :
`build/alpha29-preview/terraces-1024--7228211907433324401.png`.
## Mine vanilla et satellites
**Moyen, graine 0 :** une véritable mine vanilla de **143 pièces acceptées**
est trouvée parmi les candidats normaux. Linspection est partielle :
**16 chunks FULL** sont demandés par le test. Elle observe notamment
244 planches de chêne, 200 barrières, 102 rails et 50 toiles dans les pièces
chargées. Une salle `monster_room` avec spawner de zombies est également
observée. Le test ne prétend pas avoir exploré les 143 pièces, ni avoir
déclenché tous les spawners ou contrôlé tous les butins. La hutte de sorcière
est absente de cet échantillon ; ce nest pas une absence prouvée sur l’île.
Une lecture séparée du monde sauvegardé confirme deux spawners vanilla
`cave_spider` aux positions **156 171 -138** et **140 175 -97**, dans les
corridors correspondants (`sc=1`, `hps=1`). Reçu :
`build/alpha29-evidence/medium-0/mineshaft-spawners-saved.json`.
Sur ce même monde, **quatre satellites**, **deux villageois**, **quatre coffres**
et **une source de cascade** passent la création puis un véritable redémarrage.
Les 47 chunks FULL contrôlés conservent le pillage, les villageois déplacés
et les blocs détruits, sans duplication ni reconstruction des témoins.
Le journal dexpansion reste inchangé. Création PID 56060, rechargement
PID 56574 ; les **23 GameTests natifs** de ce dernier processus passent.
Preuves regroupées dans `build/alpha29-evidence/medium-0/`.
## Tests ciblés et portée
Les smokes terrain, appuis de mines et transit couvrent les bornes, les
orientations et les refus protégés. Le test pur de traversée couvre
0 / 42 / 2026 × 512 / 724 / 1 024 **sur un terrain synthétique portant** :
ce nest pas une matrice de neuf mondes Minecraft réellement générés.
Les contrôles natifs ci-dessus portent sur les témoins explicitement cités.
`./gradlew check build` passe : **86 tâches**, dont les **23 GameTests natifs**,
en 2 min 58 s pour cette exécution. Journal :
`build/alpha29-evidence/medium-0/gradle.log`. Ce temps nest pas un benchmark de génération.
`./gradlew assemblePack` passe également. JAR, MRpack autonome, index packwiz
et ZIP damorçage sont vérifiés localement : versions, dépendances, CRC et
empreintes concordent. Reçus `build/alpha29-artifacts.json` et
`build/alpha29-delivery-local.json`.
Restent en attente de leurs reçus :
- Contrôles natifs de **planification seule** Grand/graine42 et Petit/graine2026 ;
même réussis, ils ne prouveront pas un parcours sur des chunks FULL.
- Publication, canal packwiz et synchronisation Prism.
Pas de preuve dexécution avec structures désactivées propre à lalpha.29,
ni de nouvelle validation visuelle client ou Windows à ce stade.
## Repères pour un essai manuel
Avec les structures activées, sur un nouveau Grand de graine
`-7228211907433324401`, les entrées parcourues sont **128 252 -127** et
**128 257 213**. Sur Moyen/graine0 : **-90 235 -127** et **-90 235 111**.
`/locate structure sanctuary:industrial_transit` recherche une entrée du réseau.
Le repère **152 175 -120** désigne la mine de Moyen/graine0 : lutiliser en
spectateur pour linspection, sans supposer une arrivée sûre en survie.
+99
View File
@@ -0,0 +1,99 @@
# Alpha.30.1 — vérifications
Essais natifs sur **Mac**, le 12 septembre 2026, en serveur de développement
sans rendu client. Aucun essai Windows natif de ce correctif nest revendiqué.
## Moyen 724, graine 0
Processus **64315** : **24/24 GameTests passent**, en 2 min 30 s pour la commande.
Reçus dans `build/alpha301-evidence/medium-0/`.
Deux brèches sont rattachées à la véritable **ancienne rivière courbe**, et non
à un nouveau tracé. Sources : **13 247 79** et **-105 247 -69** ; sorties :
**13 246 82** et **-105 246 -65**. Elles traversent respectivement trois et
quatre blocs de berge. Le test sous ticks réels observe l’écoulement en dessous.
Une lecture séparée de la sauvegarde confirme des colonnes continues deau aux
sorties, de **Y245 à Y225** et de **Y246 à Y226** à ce stade de simulation.
Aucune source na été ajoutée par la brèche. Cela ne prétend pas avoir attendu
leur propagation jusquau fond du monde. Preuve : `saved-water-columns.json`.
La traversée est réellement générée : **19 modules, 28 chunks FULL, 272 blocs
entre les accès, 478 pas contrôlés et 62 points accessibles**. Entrées :
**90 248 -144** et **90 225 128**. Le sanctuaire possède son départ natif et
un sol central libre au-dessus. Les quatre satellites sont contrôlés.
Une mine vanilla de **143 pièces** est retenue ; le test inspecte un échantillon
de 16 chunks FULL. Il trouve notamment 244 planches de chêne, 200 barrières,
102 rails, 50 toiles et un spawner dans les pièces chargées. Point de visite :
**152 175 -120**. Ce nest pas une visite de la totalité de la mine.
Comparaison avec le relief de la 28 : **6 348 densités inférieures inchangées**,
et seulement **466 / 74 060** signes modifiés dans les échantillons hauts
(**0,63 %**). Ce changement est localisé dans la bande de plateau ; le reste
utilise exactement le poids global de déformation précédent.
## Grand 1 024, graine -7228211907433324401
Le premier passage natif (processus **64637**) vérifie la traversée complète :
**23 modules, 52 chunks FULL, 374 blocs entre les accès, 788 pas contrôlés et
71 points accessibles**. Entrées : **192 238 -195** et **192 277 179**.
Le sanctuaire minéral et les quatre satellites sont également retenus.
La comparaison au relief 28 conserve **8 112 densités inférieures** ;
**940 / 94 640** signes hauts diffèrent (**0,99 %**). La recherche native
échantillonnée observe des portails en ruine et un camp, mais pas de mine dans
ce témoin limité. La preuve de mine de cette livraison est celle du Moyen 0.
La région centrale de cette graine contient des lacs et bassins, sans ancienne
rivière admise. Le premier passage a donc révélé une assertion de test trop
stricte : elle exigeait une rivière sur chaque témoin, contrairement au caractère
conditionnel de lhydrologie dorigine. Le test est corrigé sans forcer une
rivière dans le générateur. Le témoin spécifique de berge de rivière peut être
exigé avec `-PsanctuaryRequireCurvedRiverBank=true` sur une graine adaptée.
Le redémarrage (processus **65123**) passe les **24/24 GameTests** :
`check build` réussit en **4 min 19 s**. Le contrôle deau inspecte 8 chunks
FULL et trouve 7 positions deau sous les deux brèches testées. Sorties :
**-14 236 41** et **-50 237 -80**. La traversée et le sol du sanctuaire sont
revérifiés. Les satellites sont rechargés après pillage, extraction dun
villageois et démolition partielle : les témoins ne sont pas reconstruits et
le villageois nest pas dupliqué. Reçus :
`build/alpha301-evidence/large-user/reload/`.
Commande de vérification :
```sh
./gradlew check build -PsanctuaryTestDiameter=1024 \
-PsanctuaryTestSeed=-7228211907433324401 -PsanctuaryDensityMaps=false \
-PsanctuaryAerial24Tests=true -PsanctuaryAerial24Phase=reload1 \
-PsanctuaryExpansionReload=true -PsanctuaryTransit29Tests=true
```
## Tests ciblés
`river30Smoke` passe : ouverture à travers une berge sèche existante, conservation
exacte des points du tracé et des cellules deau, absence de nouvelle source,
déterminisme et conservation dune berge intérieure éloignée du vide.
Une brèche de cinq blocs possède une légère descente pour l’écoulement vanilla.
Les trois tailles conservent le poids global exact sur plus de 85 % de leur
emprise circulaire ; le cœur de plateau couvre moins de 8 %.
Les brèches restent conditionnelles à la proximité dun espace ouvert. Ces tests
ne justifient pas dannoncer une cascade pour tous les bassins ou toutes les graines.
La taille Petit est couverte par les tests de champ et les tests synthétiques ;
elle na pas fait lobjet dun nouveau passage natif complet pour ce correctif.
`assemblePack` passe après le `check build` réussi ; la tâche `:sanctuary:check`
est exclue de cette seconde commande pour ne pas répéter la suite inchangée.
Le MRpack autonome, le JAR intégré et le ZIP damorçage Prism sont vérifiés
localement : versions, index, empreintes et contenu concordent. Reçus :
`build/alpha301-artifacts.json` et `build/alpha301-delivery-local.json`.
La publication et la synchronisation sont consignées dans [Distribution](packwiz.md). Créer un nouveau monde pour lessai ; aucune sauvegarde personnelle
nest régénérée.
## Distribution terminée
Release et canal publiés ; téléchargement public des trois artefacts conforme
aux empreintes locales. Deux synchronisations isolées puis deux synchronisations
de la même instance Prism **Sanctuary Beta** réussissent. Les **925 fichiers
personnels et réglages suivis** conservent leurs empreintes. Voir les versions,
commits et reçus dans [Distribution](packwiz.md#alpha301-publiée--12-septembre-2026).
+100
View File
@@ -0,0 +1,100 @@
# Alpha.30.2 — vérifications et limites
Essais sur **Mac**, le 12 septembre 2026, dans le serveur de développement.
Aucun essai Windows natif de ce correctif nest revendiqué. Les comparaisons
visuelles sont des volumes natifs de roche, sans textures, arbres ou structures,
et ne remplacent pas lessai du rendu complet dans Minecraft.
## Méthode
Le témoin principal est **Moyen 724, graine 0**. Le test compare le champ
30.1 au champ retouché aux mêmes coordonnées. Il exporte deux coupes et un
volume de **64 × 128 × 64 blocs**, choisi uniquement selon les transitions du
champ précédent. Le choix du volume ne recherche pas le meilleur résultat de
la nouvelle passe. La profondeur sous Y140 et larrivée restent inchangées.
Le volume permet de compter les composants de roche connectés par leurs six
faces. Les composants touchant ses plans de coupe sont exclus de linventaire
des fragments, car leur attache peut continuer hors du volume. Les longues
marches sont comptées sur les surfaces supérieures complètes, le long des deux
axes : suites dau moins six différences de hauteur identiques de ±1 bloc.
Ces mesures portent sur cet échantillon, pas sur toutes les graines du jeu.
Deux essais de déplacement plus simple ont été écartés avant publication : le
premier introduisait trop de fragments, le second ne réduisait pas les longues
marches du témoin. Les aperçus intermédiaires restent dans les dossiers ignorés
`build/alpha302-preview-first-pass/` et `build/alpha302-preview-second-pass/`.
## Contrôle ciblé des fragments
`surfaceRelief302Smoke` vérifie les petits amas à cheval sur un bord de chunk,
la conservation des corps connectés plus grands, labsence de remplissage du
vide, lisolation des caches entre deux samplers et la limite de déplacement
local. Le filtre sapplique au champ de roche retouché, avant les décorations ;
il ne modifie pas les constructions ni les sauvegardes personnelles.
## Témoin Moyen 724, graine 0
Le processus **67135** passe les **24/24 GameTests** ; `check build` réussit
en **5 min 18 s**. Reçus dans `build/alpha302-evidence/medium-0/`.
Dans le volume natif choisi (**X-154, Y164, Z-218**, 64 × 128 × 64), **96,72 %**
des positions restent identiques. La quantité de roche passe de **174 157 à
173 881 blocs**, soit une différence de **-0,16 %**. Les pas appartenant aux
longues suites de marches passent de **424 à 81**. Le comptage indépendant
observe **0 petit composant isolé de huit blocs ou moins**, contre cinq
composants réunissant six blocs avant la passe. Lanalyse ne compte pas les
composants coupés par la frontière du volume. Le filtre lui-même traite les
composants de 32 blocs ou moins.
Deux sorties de lancienne rivière sont vérifiées sous ticks réels :
**-105 246 -65** et **-24 246 -45**. La traversée possède **19 départs natifs**,
**29 chunks FULL** contrôlés, **272 blocs** entre ses accès et **678 pas** dans
le parcours testé. Entrées : **-136 234 82** et **136 236 82**. Le sanctuaire
possède son départ et son sol central ; les quatre satellites et une mine
vanilla sont également observés dans leurs tests respectifs.
Les mesures de planification sont distinctes du temps de rendu client : la
région hydrologique centrale prend **32,1 s**, et la recherche de traversée,
qui inclut la préparation de son terrain, **40,8 s**. Ces durées ne sajoutent
pas simplement. Le filtre a un coût ; cette livraison ne revendique pas un
gain de performance. Le test dexport exhaustif du volume bloque ponctuellement
le serveur de test ; cet export nest pas activé dans le mod livré.
## Témoin Grand 1 024, graine -7228211907433324401
Le processus **67379** passe les **24/24 GameTests** en **4 min 23 s** pour la
commande complète. Le volume natif contient **0 composant détaché de 32 blocs
ou moins**, contre **12 avant**. Le test exige de ne pas augmenter ce nombre ;
**1 024 points** rééchantillonnés dans lordre inverse, avec un autre contexte de
cache, conservent le même état roche/air.
La traversée est réellement posée : **21 départs**, **48 chunks FULL**,
**340 blocs entre les accès** et **574 pas** sur le parcours testé. Entrées :
**192 238 -195** et **192 224 145**. Le sanctuaire et les quatre satellites
passent leurs contrôles. La région centrale admet ici une rivière courbe ;
l’écoulement des brèches est contrôlé sous ticks réels dans huit chunks FULL.
Reçus : `build/alpha302-evidence/large-user/`. Les deux commandes natives
activent `sanctuaryAerial24Tests` et `sanctuaryTransit29Tests` ; le témoin Moyen
exige aussi `sanctuaryRequireCurvedRiverBank=true`. Les trois tailles conservent
leurs profils et le même filtre. Petit na pas fait lobjet dune nouvelle
visite native complète dans cette livraison. Les sauvegardes personnelles
ne sont pas régénérées ; les scénarios natifs utilisent des mondes jetables.
Les reçus de distribution sont consignés dans [packwiz](packwiz.md).
## Distribution
`assemblePack` passe après la vérification complète ; `:sanctuary:check` est
exclu de cette seconde commande pour ne pas répéter la suite inchangée. Le JAR,
le MRpack et le ZIP damorçage sont vérifiés localement : versions, index,
empreintes et contenu concordent. Reçus : `build/alpha302-artifacts.json` et
`build/alpha302-delivery-local.json`. Les reçus publics et la mise à jour Prism
sont consignés dans [Distribution](packwiz.md).
La release est publiée et ses trois artefacts sont revérifiés par téléchargement.
Deux synchronisations isolées puis deux synchronisations de la même instance
Prism sur Mac réussissent, jeu fermé. Les **925 fichiers personnels et réglages
suivis** gardent leurs empreintes. Les commits et sommes sont consignés dans
[packwiz](packwiz.md#alpha302-publiée--12-septembre-2026).
+105
View File
@@ -0,0 +1,105 @@
# Vérification alpha.30.3
## Premier témoin natif — Mac
Monde Moyen 724, graine `0`, processus `68244` : 24/24 GameTests réussis.
La comparaison utilise les densités natives enregistrées, pas un terrain simulé.
La correction finale de bornage numérique à -1, sans effet sur les blocs solides,
a ensuite été incluse dans le build de livraison.
| Profil mesuré (même graine) | Hauts terrains proches de la base 24* | Plus haut échantillon |
|---|---:|---:|
| Petit 512 | 330 / 463 (71 %) | Y344 |
| Moyen 724 | 913 / 1 288 (71 %) | Y344 |
| Grand 1 024 | 1 930 / 2 853 (68 %) | Y344 |
*Colonnes échantillonnées tous les 12 blocs, ancienne surface au-dessus de Y200,
écart final de deux blocs au plus. La base de comparaison est le relief 24 repris
en 27. Cela ne mesure ni la surface constructible exacte ni une qualité esthétique.
Les trois profils sont compilés avec le même état aléatoire natif ; seul le
monde Moyen de ce premier essai est parcouru en chunks FULL.
Le volume local contrôlé passe de cinq petits composants détachés en 30.1 à
zéro en 30.3. Le parcours inversé des échantillons donne les mêmes appartenances.
La densité sous Y170 est identique à la 30.1 aux points vérifiés.
La traversée a 19 départs de structure, 29 chunks FULL visités, portée 272.
Le sanctuaire minéral est admis et son sol natif est inspecté. Les deux navires
et deux îlots sont générés ; les tests du Blocodex et des minerais réussissent.
La rivière courbe centrale est présente. Ce témoin ne fournit pas de nouvelle
brèche de berge admissible : il ne constitue donc pas une preuve supplémentaire
de cascade de surface sous ticks, malgré le succès du test des cellules deau.
Preuves ignorées : `build/alpha303-native-medium.log`,
`build/alpha303-evidence/medium-0/`, `build/alpha303-preview/`.
Les cartes et coupes visualisent la roche seule ; aucune capture client ni
validation esthétique du joueur nest revendiquée.
## Build de livraison et témoin Petit — Mac
`./gradlew check build -PsanctuaryTestDiameter=512 -PsanctuaryTestSeed=42
-PsanctuaryTransit29Tests=true -PsanctuaryAerial24Tests=true` : réussite,
24/24 tests natifs et contrôles JVM, 4 min 53 au total sur cette machine Mac.
Ce temps comprend compilation, anciens tests et inspections ; ce nest pas
une mesure du délai de chargement dun client.
Processus `69553`. Les 15 modules de traversée possèdent leurs départs natifs,
32 chunks FULL sont parcourus, portée 204. Le sanctuaire minéral apparaît
réellement dans le marais. Quatre sites aériens, quatre coffres, deux villageois,
une source de cascade sur un îlot ; leurs coffres sont ouverts et les habitants
déplacés dans ce monde jetable par le test. Aucun petit composant détaché dans
le volume contrôlé ; 1 536 échantillons restent identiques en ordre inversé.
Les trois profils sous la graine 42 conservent 60 %, 59 % et 58 % des hauts
terrains à deux blocs au plus de la base 24. Sommets échantillonnés :
Y344 / Y346 / Y346. Une brèche de bassin produit de leau sous ticks réels ;
la région centrale de ce témoin nadmet pas de rivière courbe.
Preuves : `build/alpha303-check-build.log`, `build/alpha303-evidence/small-42/`.
## Témoin Grand signalé par le créateur — Mac
Grand 1024, graine `-7228211907433324401`, processus `69750` : 24/24 tests
natifs réussis. La traversée compte 23 départs, portée 374, avec 52 chunks FULL
parcourus et des accès vérifiés. Localisation témoin : `-187,249,-72`.
Le sanctuaire minéral a un départ valide et un véritable sol central ;
les quatre lieux aériens génèrent leurs quatre coffres et deux marchands.
La rivière courbe centrale est présente ; huit cellules deau sont constatées
sous les ouvertures après propagation native. Le volume local passe de 12
petits composants détachés à zéro. Les1 536 points contrôlés en ordre inversé
restent identiques et les 7 436 échantillons profonds sont inchangés.
Sur cette graine, les plus hauts échantillons des profils 512/724/1024 atteignent
Y322 / Y344 / Y346. Laltitude nest pas forcée à 360 sur toutes les graines.
Preuves : `build/alpha303-native-large.log`, `build/alpha303-evidence/large-user/`.
Les fichiers `verified-run.json` désignent les rapports de chaque processus ;
les autres anciens diagnostics copiés ne comptent pas comme des essais 30.3.
## Défauts corrigés pendant la vérification
Petit 512, graine 42, révélait deux défauts distincts :
- La recherche aérienne épuisait ses colonnes sur des emprises trop hautes ou
trop creuses. Le rejet de hauteur est maintenant immédiat, lappui est filtré
grossièrement avant certification et les candidats de repli couvrent le tour
de l’île. Le plan témoin obtenu emploie 8 058 colonnes, avec quatre sites.
- La chambre minérale était admise en `sanctuary:swamp27_caves`, mais ce biome
manquait au tag `has_structure/forgotten_networks`. Le départ natif était donc
refusé. Le tag inclut désormais ce biome, sans contourner la certification.
Les premiers journaux en échec sont conservés sous `build/alpha303-small42-*failure.log`.
## Limites
Essais exécutés sur Mac ; aucun essai natif Windows par lagent. Les variations
de relief exigent un nouveau monde ; aucun monde personnel nest régénéré.
Le filtrage local de connexité nest pas un audit exhaustif de l’île entière.
La présence des mines, huttes et donjons reste conditionnelle au terrain.
## Artefacts et installation
Le JAR, le MRpack et le ZIP Prism ont été contrôlés localement puis téléchargés
publiquement et comparés octet par octet. Deux synchronisations isolées, puis
deux dans la même instance Prism Mac, réussissent. Les 925 fichiers personnels
et réglages suivis conservent leurs empreintes ; un seul JAR Sanctuary 30.3 est actif.
Les reçus et empreintes sont consignés dans [packwiz](packwiz.md).
+150
View File
@@ -0,0 +1,150 @@
# Alpha.30 — vérifications
Essais du 12 septembre 2026 sur **Mac**, dans des serveurs de développement
sans rendu client. Le retour Windows du créateur concerne la 28 ; aucun essai
Windows natif de la 30 nest revendiqué. Nouveau monde nécessaire.
## Témoin Grand signalé par le créateur
**1 024 blocs, graine `-7228211907433324401`**, processus 62002 :
**24/24 GameTests passent**. Reçus dans `build/alpha30-evidence/grand-user/`.
- Traversée réellement générée : 23 modules, 52 chunks FULL, 374 blocs entre
les accès ; parcours contrôlé sur 736 pas et 71 points, avec les collisions
natives, escaliers et raccords. Entrées : **192 238 -195** et **192 256 179**.
- Rivière : tracé entre des points distants denviron **865 blocs**, 6 924 cellules
de lit et de brèche, six ouvertures. Huit chunks FULL échantillonnés ; après
120 ticks, six échantillons contiennent de leau sous les deux sorties suivies.
Ce test ne prétend pas simuler toutes les chutes jusqu’à Y0.
- Sanctuaire minéral accepté dès la recherche intérieure ; emprise depuis
**-122 131 265**, 79 × 73 × 27. Le recours exposé nest pas exercé sur ce témoin.
- Quatre satellites, deux villageois, quatre coffres et une cascade d’îlot :
44 chunks FULL contrôlés. Les témoins de pillage, déplacement et démolition
sont préparés pour le contrôle de persistance.
- Le petit échantillon de structures vanilla ne contient pas de mine. Ce nest
pas un inventaire exhaustif de l’île et aucune présence de mine ny est annoncée.
## Petit 512, graine 2026
Processus **62463**, **24/24 GameTests passent**. Reçus :
`build/alpha30-evidence/small-2026-final2/`.
La traversée produit 15 modules et leurs départs natifs, avec 204 blocs entre
les entrées. Ce contrôle est limité aux plans et départs ; aucun parcours FULL
complet de cette traversée nest revendiqué sur Petit. La rivière couvre une
distance denviron 362 blocs, avec 2 551 cellules et quatre brèches. Sept chunks
FULL sont échantillonnés ; leau coule sous les sorties contrôlées.
Le sanctuaire possède son véritable départ de structure et son sol central
est contrôlé après génération au point **-54 162 69**, avec lespace libre
au-dessus. Les quatre satellites passent sur 41 chunks FULL.
Un premier essai a révélé que le témoin de vue de l’îlot à cascade se trouvait
sur une ancienne hauteur creusée en bassin. La correction choisit une berge
sèche proche, sans ajouter de support artificiel. Une première correction trop
large épuisait le budget de colonnes ; elle a été remplacée avant validation.
La commande finale dure 2 min 8 s.
## Moyen 724, graine 0
Processus **62604**, reçus `build/alpha30-evidence/medium-0/`.
La traversée réelle passe : **19 modules, 42 chunks FULL, 272 blocs entre les
accès, 456 pas et 62 points accessibles**. Entrées : **-136 248 -53** et
**136 260 -53**. La rivière s’étend sur environ 554 blocs : 3 942 cellules,
six brèches, neuf chunks FULL contrôlés et écoulement sous les sorties suivies.
Le sol central du sanctuaire est présent en **106 189 -63**, avec lespace
libre au-dessus ; les satellites passent également.
Le test exigeant une mine sur cette graine échoue : aucun départ de mine nest
retenu parmi ses 19 candidats, y compris via la recherche native. Les **23 autres
tests passent**. Le témoin de mine de la 29 nest donc pas réutilisé comme preuve
de présence en 30. Un témoin séparé est recherché, sans forcer une implantation
incompatible. Les refus conditionnels restent un comportement voulu du jeu.
## Grand 1 024, graine 42 — témoin de mine
Processus **62979**, **24/24 GameTests passent**, y compris lexigence de trouver
une mine réelle. Reçus dans `build/alpha30-evidence/grand-42/`.
Deux départs de mines vanilla sont observés dans les candidats examinés. La
mine visitée possède **83 pièces** ; l’échantillon de 16 chunks FULL du test
contient notamment **138 planches de chêne, 120 barrières, 43 rails et 23 toiles**.
Point de visite : **6 189 6**. Le test contrôle les pièces sauvegardées et leur
aller-retour NBT ; il ne visite pas lintégralité de la mine et ne garantit pas
un spawner dans chaque corridor. Une lecture séparée de la sauvegarde confirme
un véritable spawner `cave_spider` en **46 188 25** dans un corridor :
`build/alpha30-evidence/grand-42/saved-spawners.json`. Son activation par un joueur
na pas été testée.
Deux huttes de sorcière sont également observées. La hutte visitée est en
**83 148 -396**, avec ses pièces, appuis et son mobilier vanilla.
La traversée possède **25 modules répartis sur 24 départs natifs**, avec 374 blocs entre
les entrées ; cette graine utilise le contrôle des plans, sans nouveau parcours
FULL de toute la traversée. Le sanctuaire possède son vrai départ et son sol
central. La rivière s’étend sur environ **882 blocs**, avec 5 822 cellules et
six ouvertures ; les écoulements suivis passent sous ticks réels.
`./gradlew check build` passe sur ce témoin : **88 tâches**, dont les tests purs,
les contrôles de sérialisation et les **24 GameTests natifs**, en **6 min 28 s**.
Le coût inclut toute la suite et nest pas un temps de chargement de partie.
Commande :
```sh
./gradlew check build -PsanctuaryTestDiameter=1024 -PsanctuaryTestSeed=42 \
-PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true \
-PsanctuaryTransit29Tests=true -PsanctuaryTransit29PlanOnly=true \
-PsanctuaryRequireMineshaft29=true
```
## Relief
Sur ce Grand, 8 112 densités sous Y180 sont identiques bit à bit à la 28.
Dans les coupes hautes, 4 544 / 94 640 échantillons changent de signe, soit
**4,80 %**. Deux coupes au pas dun bloc, Z0 et Z96, ont été rendues et examinées :
les plateaux redeviennent larges entre les zones où subsistent les surplombs.
Aperçu local `build/alpha30-preview/relief-1024--7228211907433324401.png`.
Les mêmes coupes ont aussi été examinées sur Petit2026 : 602 / 40 460 signes
modifiés (1,49 %), avec des sommets plus plats entre les zones déformées.
Ce contrôle de densité ne remplace pas une exploration visuelle dans le client
et ne démontre pas labsence de toute petite pointe ou masse déconnectée.
## Tests ciblés et limites
Le test pur `river30Smoke` couvre trois rayons sur une masse rocheuse synthétique :
longueur du tracé, lit dans la roche, arrivée protégée, brèches de cinq blocs
maximum, déterminisme et espaces non déformés entre les trois secteurs.
Ce nest pas une matrice de neuf mondes Minecraft natifs.
La recherche reste bornée par la taille de l’île et le nombre de candidats.
Sur le Grand, la commande native complète dure **4 min 44 s**, incluant le
lancement, la planification, les structures et les nombreux chunks demandés
par les tests. Ce temps nest ni un temps de rendu client, ni un benchmark
comparatif avec la 28. Aucun objectif chiffré de performance nest revendiqué.
Les tests vérifient des occurrences et parcours précis, pas une garantie
universelle sur toutes les graines. Les mines restent conditionnelles, et le
sanctuaire peut encore être refusé si aucune implantation na dattache valable.
Les réglages, le Blocodex et la suite générale restent soumis à `check build`.
## Redémarrage et artefacts
Grand1 024, graine42, processus **63276** : **24/24 tests passent après un
redémarrage réel** (2 min 14 s). Les 47 chunks FULL du contrôle aérien conservent
coffres pillés, villageois déplacés et destructions préparées à la création,
sans duplication ni reconstruction des témoins. Les départs de mine et leurs
pièces, le sol du sanctuaire et les écoulements échantillonnés passent à nouveau.
Le journal dexpansion reste inchangé. Preuves :
`build/alpha30-evidence/grand-42-reload/` et `build/alpha30-reload.log`.
`./gradlew assemblePack` passe après `check build` (le second lancement exclut
la tâche `:sanctuary:check` déjà réussie). Le JAR, le MRpack autonome et le ZIP
damorçage Prism passent la vérification locale : versions, dépendances exactes,
index et empreintes concordent. Reçus : `build/alpha30-artifacts.json`,
`build/alpha30-delivery-local.json`. Les fichiers du client Windows nont pas
été modifiés. La publication et la synchronisation de linstance Mac sont
suivies dans [la distribution](packwiz.md).
La release alpha.30 est publiée depuis `bf327b7`. Les archives publiques ont les
mêmes empreintes que les fichiers vérifiés ; le canal et linstance Prism Mac
sont synchronisés deux fois. Les 925 fichiers personnels suivis sont inchangés.
Les reçus de distribution et empreintes sont dans [packwiz](packwiz.md).
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.28
pack_version=0.1.0-alpha.28
mod_version=0.1.0-alpha.30.3
pack_version=0.1.0-alpha.30.3
maven_group=fr.koka.sanctuary
+51
View File
@@ -56,6 +56,11 @@ tasks.named('runGameTest') {
systemProperty('sanctuary.test.generateStructures', providers.gradleProperty('sanctuaryGenerateStructures').getOrElse('true'))
systemProperty('sanctuary.test.selective22', providers.gradleProperty('sanctuarySelective22Tests').getOrElse('false'))
systemProperty('sanctuary.transit.plannerDiagnostics', providers.gradleProperty('sanctuaryTransitDiagnostics').getOrElse('false'))
systemProperty('sanctuary.test.transit29', providers.gradleProperty('sanctuaryTransit29Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit29.planOnly', providers.gradleProperty('sanctuaryTransit29PlanOnly').getOrElse('false'))
systemProperty('sanctuary.test.requireMineshaft29', providers.gradleProperty('sanctuaryRequireMineshaft29').getOrElse('false'))
systemProperty('sanctuary.test.requireCurvedRiverBank', providers.gradleProperty('sanctuaryRequireCurvedRiverBank').getOrElse('false'))
systemProperty('sanctuary.expansion.structureDiagnostics', providers.gradleProperty('sanctuaryStructureDiagnostics').getOrElse('false'))
systemProperty('sanctuary.test.heritage21', providers.gradleProperty('sanctuaryHeritage21Tests').getOrElse('false'))
systemProperty('sanctuary.test.lostCity21', providers.gradleProperty('sanctuaryLostCity21Tests').getOrElse('false'))
systemProperty('sanctuary.test.transit21', providers.gradleProperty('sanctuaryTransit21Tests').getOrElse('false'))
@@ -631,3 +636,49 @@ tasks.register('cavesRelief28Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.CavesRelief28Smoke'
}
tasks.named('check') { dependsOn('cavesRelief28Smoke') }
// Fine ledges retain the alpha28 field below the upper relief.
tasks.register('terraces29Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.Terraces29Smoke'
}
tasks.named('check') { dependsOn('terraces29Smoke') }
[
transit29Smoke: 'fr.koka.sanctuary.worldgen.transit.Transit29Smoke',
mineshaftAdmission29Smoke: 'fr.koka.sanctuary.expansion.MineshaftAdmission29Smoke'
].each { taskName, harness ->
tasks.register(taskName, JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = harness
}
tasks.named('check') { dependsOn(taskName) }
}
tasks.register('river30Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.River30Smoke'
}
tasks.named('check') { dependsOn('river30Smoke') }
tasks.register('surfaceRelief302Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.SurfaceRelief302Smoke'
}
tasks.named('check') { dependsOn('surfaceRelief302Smoke') }
tasks.register('balancedRelief303Smoke', JavaExec) {
group = 'verification'
dependsOn tasks.named('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.BalancedRelief303Smoke'
}
tasks.named('check') { dependsOn('balancedRelief303Smoke') }
@@ -40,27 +40,64 @@ public final class Alpha28NativeStructuresGameTests {
report.put("scope", "Native structures globally disabled"); report.put("passed", true); write(report); helper.succeed(); return;
}
var root = host.source().islands().stream().filter(island -> island.origin()).findFirst().orElseThrow();
var candidates = ExpansionStructures23.candidates(root).stream().limit(12).toList();
var candidates = ExpansionStructures23.candidates(root).stream().limit(24).toList();
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
var firstTwelve = new TreeMap<String, Integer>(); int candidateIndex = 0;
for (var candidate : candidates) {
var chunk = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true);
for (var start : chunk.getAllStarts().values()) {
if (!start.isValid()) continue;
String id = registry.getKey(start.getStructure()).toString(); observed.merge(id, 1, Integer::sum);
if (candidateIndex < 12) firstTwelve.merge(id, 1, Integer::sum);
if (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
}
candidateIndex++;
}
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
report.put("first_twelve_starts", firstTwelve);
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29") && found.keySet().stream().noneMatch(id -> id.contains("mineshaft"))) {
var islandsBefore = List.copyOf(host.source().islands());
var wanted = net.minecraft.core.HolderSet.direct(registry.get(Identifier.parse("minecraft:mineshaft")).orElseThrow(),
registry.get(Identifier.parse("minecraft:mineshaft_mesa")).orElseThrow());
// Same finite real-start path as /locate; no injected plan, reference increment or expansion.
var located = ExpansionStructures23.findNearest(level, wanted, BlockPos.ZERO, 8, false);
helper.assertTrue(islandsBefore.equals(host.source().islands()), "Native /locate never adds an expansion or changes the island descriptors");
var locateProof = new LinkedHashMap<String, Object>();
locateProof.put("invoked", true); locateProof.put("found", located != null);
locateProof.put("islands_unchanged", true); locateProof.put("skip_referenced", false);
locateProof.put("root_candidates_available", ExpansionStructures23.candidates(root).size());
report.put("locate_mineshaft", locateProof);
if (located != null) {
String id = located.getSecond().unwrapKey().orElseThrow().identifier().toString();
for (var candidate : ExpansionStructures23.candidates(root)) {
var start = level.getChunk(candidate.chunk().x(), candidate.chunk().z(), ChunkStatus.STRUCTURE_STARTS, true)
.getStartForStructure(located.getSecond().value());
if (start != null && start.isValid() && start.getPieces().getFirst().getLocatorPosition().equals(located.getFirst())) {
found.put(id, start); locateProof.put("start_chunk", List.of(candidate.chunk().x(), candidate.chunk().z())); break;
}
}
helper.assertTrue(found.containsKey(id), "The native /locate result belongs to a real saved start");
locateProof.put("id", id); locateProof.put("position", located.getFirst().toShortString());
}
} else report.put("locate_mineshaft", Map.of("invoked", false));
var full = new LinkedHashSet<ChunkPos>();
// Only a few actual pieces are visited: this is not full-map pregeneration.
// Visit actual corridor bodies (including both endpoint chunks), not just four
// piece centres that could all belong to unadorned rooms. No graph is truncated.
var pieceContext = StructurePieceSerializationContext.fromLevel(level);
int mineFamilies = (int) found.keySet().stream().filter(id -> id.contains("mineshaft")).count();
for (var entry : found.entrySet()) {
full.add(entry.getValue().getChunkPos());
int remaining = entry.getKey().equals("minecraft:swamp_hut") ? 4 : 4;
for (var piece : entry.getValue().getPieces()) {
if (remaining-- <= 0) break;
var b = piece.getBoundingBox(); full.add(new ChunkPos(b.getCenter().getX() >> 4, b.getCenter().getZ() >> 4));
if (entry.getKey().equals("minecraft:swamp_hut")) b.intersectingChunks().forEach(full::add);
var pieces = entry.getValue().getPieces();
if (entry.getKey().contains("mineshaft")) pieces = pieces.stream()
.filter(piece -> piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor)
.sorted(Comparator.comparingInt(piece -> {
var tag = piece.createTag(pieceContext);
return tag.getBoolean("hr").orElse(false) ? 0 : tag.getBoolean("sc").orElse(false) ? 1 : 2;
})).limit(mineFamilies > 1 ? 2 : 4).toList();
for (var piece : pieces) {
var proposed = new LinkedHashSet<>(full); piece.getBoundingBox().intersectingChunks().forEach(proposed::add);
if (proposed.size() <= 24) full.addAll(proposed);
}
}
// A few additional real generation chunks can contain native monster_room attempts.
@@ -90,6 +127,13 @@ public final class Alpha28NativeStructuresGameTests {
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var context = StructurePieceSerializationContext.fromLevel(level);
var proofs = new LinkedHashMap<String, Object>();
var census = StructureCensusService.snapshot(level.getServer()).evidence().sites().values();
var monsterPositions = new HashSet<BlockPos>();
for (var site : census) {
var key = site.key();
if (key.id().equals(StructureCensusMemory.MONSTER_ROOM) && key.dimension().equals(level.dimension().identifier().toString()))
monsterPositions.add(new BlockPos(key.x(), key.y(), key.z()));
}
for (var entry : found.entrySet()) {
var expected = entry.getValue(); var cp = expected.getChunkPos();
var actual = level.getChunk(cp.x(), cp.z(), ChunkStatus.FULL, true)
@@ -99,14 +143,54 @@ public final class Alpha28NativeStructuresGameTests {
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Native piece NBT roundtrip: " + entry.getKey());
var counts = new TreeMap<String, Integer>(); var positions = new HashSet<BlockPos>();
var spawners = new ArrayList<Object>(); var corridorFlags = new ArrayList<Object>();
var spawnerTypes = new HashMap<BlockPos, String>();
for (var piece : actual.getPieces()) {
var b = piece.getBoundingBox();
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
var flags = piece.createTag(context);
corridorFlags.add(Map.of("box", b.toString(), "fully_visited", b.intersectingChunks().allMatch(full::contains),
"rails", flags.getBoolean("hr").orElse(false), "spider_corridor", flags.getBoolean("sc").orElse(false),
"spawner_placed", flags.getBoolean("hps").orElse(false)));
}
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
for (int y = b.minY(); y <= b.maxY(); y++) {
for (int y = b.minY() - 1; y <= b.maxY() + 1; y++) {
BlockPos pos = new BlockPos(x, y, z); if (!positions.add(pos)) continue;
var state = level.getBlockState(pos);
if (!state.isAir()) counts.merge(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), 1, Integer::sum);
if (state.is(Blocks.SPAWNER)) {
helper.assertTrue(level.getBlockEntity(pos) instanceof SpawnerBlockEntity, "A mine spawner has its actual native block entity");
var saved = ((SpawnerBlockEntity) level.getBlockEntity(pos)).saveWithFullMetadata(level.registryAccess());
String mob = saved.getCompoundOrEmpty("SpawnData").getCompoundOrEmpty("entity").getStringOr("id", "");
spawnerTypes.put(pos, mob);
String origin = monsterPositions.contains(pos) ? "monster_room" : "mineshaft";
if (entry.getKey().contains("mineshaft")) {
if (origin.equals("monster_room")) helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").contains(mob),
"A recorded vanilla dungeon inside the mine retains its own monster type");
else {
var flags = piece.createTag(context);
helper.assertTrue(mob.equals("minecraft:cave_spider") && flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false),
"A native mine spawner agrees with its cave-spider piece flags");
}
}
spawners.add(Map.of("position", pos.toShortString(), "origin", origin, "entity", mob, "nbt", saved.toString()));
}
}
}
if (piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
var flags = piece.createTag(context);
if (flags.getBooleanOr("sc", false) && flags.getBooleanOr("hps", false) && b.intersectingChunks().allMatch(full::contains)) {
boolean actualSpider = spawnerTypes.entrySet().stream().anyMatch(e -> b.isInside(e.getKey()) && e.getValue().equals("minecraft:cave_spider"));
// A later successful native monster_room may replace a mine's spawner. Its real
// census coordinate must then lie on this corridor's native spawner centreline.
boolean recordedReplacement = false;
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? b.getZSpan() : b.getXSpan();
for (int z = 1; z < length; z++) {
var at = SwampHutPlacement28.worldPosition(piece,1,0,z);
if (monsterPositions.contains(at) && spawnerTypes.containsKey(at)) recordedReplacement = true;
}
helper.assertTrue(actualSpider || recordedReplacement, "A fully visited sc/hps corridor has its actual cave-spider spawner or an evidenced later vanilla dungeon");
}
}
if (entry.getKey().equals("minecraft:swamp_hut")) for (var pier : SwampHutPlacement28.piers(piece)) {
@@ -118,23 +202,30 @@ public final class Alpha28NativeStructuresGameTests {
.filter(e -> e.getKey().endsWith("_planks") || e.getKey().endsWith("_fence") || e.getKey().equals("minecraft:rail"))
.mapToInt(Map.Entry::getValue).sum() > 0, "A selected mine creates actual native timber/rail geometry");
proofs.put(entry.getKey(), Map.of("start_chunk", List.of(cp.x(), cp.z()), "pieces", actual.getPieces().size(),
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString()));
"blocks_in_loaded_pieces", counts, "native_tag", tag.toString(), "visit", actual.getPieces().getFirst().getLocatorPosition().toShortString(),
"corridor_flags", corridorFlags, "spawners", spawners));
}
var rooms = new ArrayList<Object>();
for (var site : StructureCensusService.snapshot(level.getServer()).evidence().sites().values()) {
for (var site : census) {
var key = site.key();
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
if (!full.contains(cp)) continue;
var at = new BlockPos(key.x(), key.y(), key.z());
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true));
var spawnerNbt = ((SpawnerBlockEntity) level.getBlockEntity(at)).saveWithFullMetadata(level.registryAccess());
helper.assertTrue(List.of("minecraft:zombie", "minecraft:skeleton", "minecraft:spider").stream().anyMatch(spawnerNbt.toString()::contains), "Vanilla dungeon retains a real vanilla monster type");
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true, "spawner_nbt", spawnerNbt.toString()));
}
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
report.put("mineshaft_found", found.keySet().stream().anyMatch(id -> id.contains("mineshaft")));
boolean mineFound = found.keySet().stream().anyMatch(id -> id.contains("mineshaft"));
report.put("mineshaft_found", mineFound);
report.put("mineshaft_required", Boolean.getBoolean("sanctuary.test.requireMineshaft29"));
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
report.put("monster_room_found", !rooms.isEmpty());
report.put("scope", "At most12 normal start candidates and24 FULL chunks; absence is reported, never replaced by synthetic fixtures");
report.put("scope", "Sample of24 normal start candidates, optional real /locate when a mine is required, and at most24 requested FULL chunks; the first12 remain separately reported. Corridor flags and optional spawners do not impose random-content quotas");
if (Boolean.getBoolean("sanctuary.test.requireMineshaft29"))
helper.assertTrue(mineFound, "The required mine witness has a real native start and inspected FULL timber/rail geometry");
}
private static void write(Map<String, Object> report) throws Exception {
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
@@ -0,0 +1,68 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.worldgen.*;
import java.nio.file.*;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
public final class River30WorldGameTests {
@GameTest(maxTicks=12000)
public void riverAndOpenBanksUseRealTerrain(GameTestHelper helper) throws Exception {
var level=helper.getLevel(); var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
var binding=generator.starterContext();var plan=PopulationHydrologyRuntime.regionPlan(binding.generator(),binding.random(),new IslandCapacity.Region(0,0)).water();
var report=new LinkedHashMap<String,Object>();report.put("seed",level.getSeed());report.put("diameter",generator.initialDiameter());
report.put("process_id",ProcessHandle.current().pid());report.put("river_cells",plan.cells().size());report.put("features",plan.features());report.put("spills",plan.spills());
report.put("mineral",generator.mineralSanctuary23());
Path initial=Path.of("diagnostics/alpha301/river-planned.json"); Files.createDirectories(initial.getParent());
Files.writeString(initial,new GsonBuilder().setPrettyPrinting().create().toJson(report));
report.put("curved_river_count",plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).count());
if(level.structureManager().shouldGenerateStructures()&&generator.experimentalStructures())
helper.assertFalse(generator.mineralSanctuary23().isEmpty(),"The mineral sanctuary must be present in the release witness");
var selected=plan.cells().stream().filter(PopulationHydrology.Cell::hasWater).toList();
var witnesses=new ArrayList<PopulationHydrology.Cell>();
for(int i=0;i<6;i++)witnesses.add(selected.get(i*(selected.size()-1)/5));
var falls=plan.spills().stream().filter(f -> plan.features().stream().anyMatch(feature -> feature.kind()==PopulationHydrology.Kind.OUTLET && feature.id()==f.id())).limit(2).toList();
if(Boolean.getBoolean("sanctuary.test.requireCurvedRiverBank"))
helper.assertTrue(falls.stream().anyMatch(f -> plan.features().stream().anyMatch(feature -> feature.id()==f.fromFeatureId() && feature.kind()==PopulationHydrology.Kind.RIVER)),
"The required witness opens a bank of an original curved river");
helper.assertTrue(plan.features().stream().noneMatch(f -> f.id()==0x300000000L),"The rejected grid river is absent");var chunks=new LinkedHashSet<ChunkPos>();
witnesses.forEach(c->chunks.add(new ChunkPos(c.x()>>4,c.z()>>4)));
for(var fall:falls){chunks.add(new ChunkPos(fall.source().x()>>4,fall.source().z()>>4));chunks.add(new ChunkPos(fall.outlet().x()>>4,fall.outlet().z()>>4));}
var mineral=generator.mineralSanctuary23().chamber();
if(mineral!=null){
var key=fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryStructures23.KEY;
var structure=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.STRUCTURE).getOrThrow(key).value();
var cp=new ChunkPos(mineral.startChunkX(),mineral.startChunkZ());
var start=level.getChunk(cp.x(),cp.z(),ChunkStatus.STRUCTURE_STARTS,true).getStartForStructure(structure);
helper.assertTrue(start!=null&&start.isValid()&&start.getPieces().size()==1,"The mineral plan produces its actual native structure start");
chunks.add(cp);
}
var iterator=chunks.iterator();Runnable[] step=new Runnable[1];
step[0]=()->{
if(iterator.hasNext()) {var c=iterator.next();level.getChunk(c.x(),c.z(),ChunkStatus.FULL,true);helper.runAtTickTime(helper.getTick()+1,()->step[0].run());return;}
chunks.forEach(c->level.getChunkSource().addTicketWithRadius(TicketType.DRAGON,c,2));
helper.runAtTickTime(helper.getTick()+120,()->{
try {
if(mineral!=null){
var floor=new BlockPos(mineral.centerX(),mineral.minY()+2,mineral.centerZ());
helper.assertTrue(level.getBlockState(floor).isSolid()&&level.getBlockState(floor.above()).isAir(),"The central mineral floor and guardian space are really generated");
report.put("mineral_native_floor",List.of(floor.getX(),floor.getY(),floor.getZ()));
}
for(var c:witnesses)helper.assertTrue(level.getFluidState(new BlockPos(c.x(),c.waterY(),c.z())).is(net.minecraft.tags.FluidTags.WATER),"Generated river water remains at its supported local bed");
int flowing=0;
for(var fall:falls)for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++)
if(level.getFluidState(new BlockPos(fall.outlet().x()+dx,fall.outlet().y()-3,fall.outlet().z()+dz)).is(net.minecraft.tags.FluidTags.WATER)){flowing++;}
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
report.put("tested_outlets",falls); report.put("full_chunks",chunks.size());report.put("flowing_below_openings",flowing);report.put("passed",true);
Path out=Path.of("diagnostics/alpha301/river.json");Files.createDirectories(out.getParent());Files.writeString(out,new GsonBuilder().setPrettyPrinting().create().toJson(report));helper.succeed();
}catch(Exception error){throw new RuntimeException(error);}finally{chunks.forEach(c->level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON,c,2));}
});
};step[0].run();
}
}
@@ -0,0 +1,150 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Compares registered native fields; does not place blocks or load additional chunks. */
public final class Terraces29WorldGameTests {
@GameTest(maxTicks = 2400)
public void broadProvincesRestorePlateausAndRaiseNativeMasses(GameTestHelper helper) throws Exception {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var field = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION,
SanctuaryMod.id("population_" + generator.capacity().players()))).value();
helper.assertTrue(field.balanced303() && !field.terraces29() && !field.surface302(), "Alpha30.3 removes fine bands and uses broad geological provinces");
var random = generator.starterContext().random();
var current = random.getSampler(field); var previous = random.getSampler(field.legacy301());
var context = SamplerContext.builder().enableCaches().build();
var report = new LinkedHashMap<String, Object>();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid());
int radius = Math.min(400, generator.initialDiameter() / 2), lowerSamples = 0, unchanged = 0, changed = 0;
for (int z = -radius; z <= radius; z += 32) for (int x = -radius; x <= radius; x += 32) {
for (int y = 32; y <= 170; y += 13) {
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Deep density is exactly alpha30.1");
lowerSamples++;
}
for (int y = 171; y <= 380; y++) {
boolean old = previous.sampleValue(context, x, y, z) > 0;
boolean now = current.sampleValue(context, x, y, z) > 0;
if (old == now) unchanged++; else changed++;
if (Math.hypot(x, z) <= 48) helper.assertTrue(old == now, "Arrival silhouette is unchanged");
}
}
helper.assertTrue(changed > 0, "The upper field now combines broad plateaus and raised rock");
report.put("unchanged_lower_density_samples", lowerSamples);
report.put("unchanged_upper_samples", unchanged); report.put("changed_upper_samples", changed);
var sections = new LinkedHashMap<String, Object>();
for (int z : new int[]{0, 96}) {
var before = new ArrayList<String>(); var after = new ArrayList<String>();
for (int y = 365; y >= 145; y--) {
var a = new StringBuilder(); var b = new StringBuilder();
for (int x = -radius; x <= radius; x++) {
a.append(previous.sampleValue(context, x, y, z) > 0 ? '#' : '.');
b.append(current.sampleValue(context, x, y, z) > 0 ? '#' : '.');
}
before.add(a.toString()); after.add(b.toString());
}
sections.put("z" + z, Map.of("before", before, "after", after, "min_x", -radius, "max_y", 365, "step", 1));
}
report.put("sections", sections);
// One block-resolution volume, chosen using the old field's surface transitions.
// Both versions sample the exact same coordinates, without structures or decoration.
int bestX=80,bestZ=80,bestScore=-1;
for(int cz=-radius+48;cz<=radius-48;cz+=64)for(int cx=-radius+48;cx<=radius-48;cx+=64){
if(Math.hypot(cx,cz)<80||Math.hypot(cx,cz)>generator.capacity().radius()*.78)continue;
int score=0;
for(int dz=-24;dz<=24;dz+=8)for(int dx=-24;dx<=24;dx+=8){
int last=-1;
for(int y=290;y>=175;y--){
int solid=previous.sampleValue(context,cx+dx,y,cz+dz)>0?1:0;
if(last>=0&&last!=solid)score++;last=solid;
}
}
if(score>bestScore){bestScore=score;bestX=cx;bestZ=cz;}
}
var oldVolume=new ArrayList<String>();var newVolume=new ArrayList<String>();
int size=64,minY=171,maxY=362;
for(int y=minY;y<=maxY;y++)for(int z=bestZ-size/2;z<bestZ+size/2;z++){
var a=new StringBuilder();var b=new StringBuilder();
for(int x=bestX-size/2;x<bestX+size/2;x++){
a.append(previous.sampleValue(context,x,y,z)>0?'#':'.');
b.append(current.sampleValue(context,x,y,z)>0?'#':'.');
}oldVolume.add(a.toString());newVolume.add(b.toString());
}
report.put("volume",Map.of("min_x",bestX-size/2,"min_z",bestZ-size/2,"min_y",minY,
"size_x",size,"size_z",size,"size_y",maxY-minY+1,"before",oldVolume,"after",newVolume));
int oldFragments=tinyComponents(oldVolume,size,maxY-minY+1);
int newFragments=tinyComponents(newVolume,size,maxY-minY+1);
report.put("tiny_components_before",oldFragments);report.put("tiny_components_after",newFragments);
helper.assertTrue(newFragments<=oldFragments,"The retouched volume must not introduce extra detached fragments");
var reverseContext=SamplerContext.builder().enableCaches().build();int reversed=0;
for(int y=maxY;y>=minY;y-=8)for(int z=bestZ+size/2-1;z>=bestZ-size/2;z-=8)
for(int x=bestX+size/2-1;x>=bestX-size/2;x-=8){
boolean expected=newVolume.get((y-minY)*size+z-(bestZ-size/2)).charAt(x-(bestX-size/2))=='#';
helper.assertTrue((current.sampleValue(reverseContext,x,y,z)>0)==expected,"Rock membership is independent of cache and reverse sample order");reversed++;
}
report.put("reverse_order_samples",reversed);
// Broad top views compare all three supported sizes under the same native noise seed.
var sizes = new ArrayList<Object>();
for (int players : new int[]{5,10,20}) {
var f = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_"+players))).value();
var now = random.getSampler(f); var old = random.getSampler(f.legacy27());
var global = random.getSampler(f.legacy301());
int r = (int)Math.round(f.capacity().radius());
var columns = new ArrayList<Object>(); int retained=0,land=0,highest=0;
for(int z=-r;z<=r;z+=12)for(int x=-r;x<=r;x+=12){
int a=0,b=0,c=0;
for(int y=380;y>=32;y-=2){
if(a==0 && old.sampleValue(context,x,y,z)>0)a=y;
if(b==0 && now.sampleValue(context,x,y,z)>0)b=y;
if(c==0 && global.sampleValue(context,x,y,z)>0)c=y;
if(a>0&&b>0&&c>0)break;
}
if(a>=200){land++;if(Math.abs(a-b)<=2)retained++;}
highest=Math.max(highest,b); columns.add(List.of(x,z,a,b,c));
}
helper.assertTrue(highest<=358,"Natural summits leave room below the build ceiling");
helper.assertTrue(retained>land*.25,"Each size retains substantial original uplands, rather than changing every slope");
sizes.add(Map.of("diameter",r*2,"columns",columns,"unchanged_uplands",retained,"uplands",land,"highest_sample",highest));
}
report.put("sizes",sizes);
report.put("passed", true);
Path path = Path.of("diagnostics/alpha303", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
Files.createDirectories(path.getParent());
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
helper.succeed();
}
private static int tinyComponents(List<String> rows,int size,int height){
int plane=size*size,total=plane*height,tiny=0;
boolean[] visited=new boolean[total];int[] queue=new int[total];
for(int start=0;start<total;start++){
if(visited[start]||rows.get(start/size).charAt(start%size)!='#')continue;
int head=0,count=1;queue[0]=start;visited[start]=true;boolean boundary=false;
while(head<count){
int p=queue[head++],x=p%size,z=(p/size)%size,y=p/plane;
if(x==0||x==size-1||z==0||z==size-1||y==0||y==height-1)boundary=true;
int[] neighbours={x>0?p-1:-1,x<size-1?p+1:-1,z>0?p-size:-1,z<size-1?p+size:-1,y>0?p-plane:-1,y<height-1?p+plane:-1};
for(int next:neighbours)if(next>=0&&!visited[next]&&rows.get(next/size).charAt(next%size)=='#'){
visited[next]=true;queue[count++]=next;
}
}
if(!boundary&&count<=32)tiny++;
}
return tiny;
}
}
@@ -342,15 +342,18 @@ public final class Transit21WorldGameTests {
static Map<String, Object> verifyAccess(GameTestHelper helper, TransitPlan21 plan) {
var level = helper.getLevel();
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
boolean fitted29 = plan.diagnostic().startsWith("accepted29");
int approachHalfWidth = fitted29 ? 1 : 2;
int approachDepth = fitted29 ? 1 : 3;
helper.assertTrue(accesses.size() == 2, "Native transit requires exactly two surface gates");
Predicate<BlockPos> inside = pos -> plan.contains(pos.getX(), pos.getY(), pos.getZ())
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
|| accesses.stream().anyMatch(m -> pos.getX() >= m.centerX() - approachHalfWidth && pos.getX() <= m.centerX() + approachHalfWidth
&& pos.getZ() >= m.minZ() - approachDepth && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var start = TransitPiece21.locator(accesses.getFirst());
Predicate<BlockPos> naturalApproach = pos -> accesses.stream().anyMatch(m ->
pos.getX() >= m.centerX() - 2 && pos.getX() <= m.centerX() + 2
&& pos.getZ() >= m.minZ() - 3 && pos.getZ() < m.minZ()
pos.getX() >= m.centerX() - approachHalfWidth && pos.getX() <= m.centerX() + approachHalfWidth
&& pos.getZ() >= m.minZ() - approachDepth && pos.getZ() < m.minZ()
&& pos.getY() >= m.surfaceY() && pos.getY() <= m.surfaceY() + 2);
var reached = walk(helper, start, inside, naturalApproach); int targets = 0;
var arrivals = new ArrayList<Map<String, Object>>();
@@ -364,7 +367,8 @@ public final class Transit21WorldGameTests {
var gate = TransitPiece21.locator(access);
helper.assertTrue(reached.containsKey(Foot.at(gate)), "The second independent surface gate is not connected by real stairs");
int approaches = 0;
for (int x = access.centerX() - 2; x <= access.centerX() + 2; x++) for (int z = access.minZ() - 3; z < access.minZ(); z++) {
for (int x = access.centerX() - approachHalfWidth; x <= access.centerX() + approachHalfWidth; x++)
for (int z = access.minZ() - approachDepth; z < access.minZ(); z++) {
var high = new BlockPos(x, access.surfaceY() + 1, z);
var low = high.below(); var position = standable(level, high) ? high : low;
helper.assertTrue(standable(level, position) && reached.containsKey(Foot.at(position)),
@@ -0,0 +1,130 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.transit.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
/** One actual island and its published route. No injected starts, synthetic terrain or expansion. */
public final class Transit29WorldGameTests {
private static final Path OUT = Path.of("diagnostics/transit29");
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 12000)
public void throughRouteHasTwoNativeAccesses(GameTestHelper helper) throws Exception {
if (!Boolean.getBoolean("sanctuary.test.transit29")) { helper.succeed(); return; }
var report = new LinkedHashMap<String, Object>();
try {
var level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
var plan = generator.transitPlan21();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("process_id", ProcessHandle.current().pid()); report.put("planner", plan.diagnostic());
report.put("modules", plan.modules().size()); report.put("probes", plan.samples());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
if (!level.structureManager().shouldGenerateStructures()) {
helper.assertTrue(plan.isEmpty(), "Structures OFF leaves no transit reservation");
report.put("passed", true); write(report); helper.succeed(); return;
}
helper.assertFalse(plan.isEmpty(), "A structures-enabled Sanctuary world must retain its main traversal: " + plan.diagnostic());
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
helper.assertTrue(accesses.size() == 2, "The complete traversal has two independent surface accesses");
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
helper.assertTrue(span >= generator.initialDiameter() / 4, "The main route spans at least a quarter of the island diameter");
report.put("span", span);
var holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(TransitStructures21.KEY);
var starts = new LinkedHashMap<ChunkPos, Integer>();
var context = StructurePieceSerializationContext.fromLevel(level);
var biomes = new TreeMap<String, Integer>();
var chunks = new LinkedHashSet<ChunkPos>();
for (var module : plan.modules()) {
var cp = new ChunkPos(module.startChunkX(), module.startChunkZ());
if (!starts.containsKey(cp)) {
var chunk = level.getChunk(cp.x(), cp.z(), ChunkStatus.STRUCTURE_STARTS, true);
var start = chunk.getStartForStructure(holder.value());
helper.assertTrue(start != null && start.isValid(), "Every planned local transit start exists: " + cp);
int expected = plan.modulesForChunk(cp.x(), cp.z()).size();
helper.assertTrue(start.getPieces().size() == expected, "No planned module was lost during native structure generation");
var tag = start.createTag(context, cp);
var restored = StructureStart.loadStaticStart(context, tag, level.getSeed());
helper.assertTrue(restored != null && tag.equals(restored.createTag(context, cp)), "Every native transit start roundtrips its full saved geometry");
for (var piece : start.getPieces())
helper.assertFalse(piece instanceof TransitPiece21 transit
&& transit.module().kind() == TransitPlan21.Kind.JUNCTION,
"Junctions close their unopened walls with the corrected renderer");
starts.put(cp, expected);
}
TransitPiece21.bounds(module).intersectingChunks().forEach(chunks::add);
if (module.kind() == TransitPlan21.Kind.ACCESS) {
for (int x = module.centerX() - 2; x <= module.centerX() + 2; x++)
for (int z = module.minZ() - 3; z < module.minZ(); z++) chunks.add(new ChunkPos(x >> 4, z >> 4));
}
}
helper.assertTrue(chunks.size() <= 160, "The native traversal witness stays inside its bounded footprint");
report.put("local_starts", starts.size()); report.put("planned_footprint_chunks", chunks.size());
boolean planOnly = Boolean.getBoolean("sanctuary.test.transit29.planOnly");
report.put("plan_only", planOnly);
if (planOnly) {
report.put("full_chunks", 0);
report.put("surface_accesses", accesses.stream().map(m -> TransitPiece21.locator(m).toShortString()).toList());
report.put("scope", "Published native plan and all STRUCTURE_STARTS with saved-piece roundtrip; no FULL blocks or physical traversal checked in this case");
report.put("passed", true); write(report); helper.succeed(); return;
}
report.put("full_chunks", chunks.size());
var iterator = chunks.iterator(); Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (iterator.hasNext()) {
var at = iterator.next(); report.put("stage", "FULL " + at);
level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
for (var module : plan.modules()) {
var at = TransitPiece21.locator(module);
biomes.merge(level.getBiome(at).getRegisteredName(), 1, Integer::sum);
}
int closedWallChecks = 0;
for (var module : plan.modules()) if (module.kind() == TransitPlan21.Kind.JUNCTION)
for (int direction = 0; direction < 4; direction++) if ((module.openings() & 1 << direction) == 0)
for (int across = -2; across <= 2; across++) {
int x = direction == 1 ? module.maxX() : direction == 3 ? module.minX() : module.centerX() + across;
int z = direction == 0 ? module.minZ() : direction == 2 ? module.maxZ() : module.centerZ() + across;
var at = new net.minecraft.core.BlockPos(x, module.minY() + 2, z);
helper.assertTrue(level.getBlockState(at).isSolid(), "An unopened junction wall remains solid at " + at);
closedWallChecks++;
}
report.put("closed_wall_checks", closedWallChecks);
report.put("module_biomes", biomes);
report.put("walk", Transit21WorldGameTests.verifyAccess(helper, plan));
var nearest = TransitStructures21.findNearest(level, HolderSet.direct(holder), net.minecraft.core.BlockPos.ZERO, 128, false);
helper.assertTrue(nearest != null && accesses.stream().anyMatch(m -> TransitPiece21.locator(m).equals(nearest.getFirst())),
"The real structure locator returns one of the walkable surface gates");
report.put("locate", nearest.getFirst().toShortString());
report.put("passed", true); write(report); helper.succeed();
} catch (Exception failure) { fail(report, failure); throw new IllegalStateException("Transit29 native traversal failed", failure); }
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
} catch (Exception failure) { fail(report, failure); throw failure; }
}
private static void write(Map<String, Object> report) throws Exception {
Files.createDirectories(OUT); Files.writeString(OUT.resolve("native.json"), JSON.toJson(report) + "\n");
}
private static void fail(Map<String, Object> report, Exception failure) {
report.put("passed", false); report.put("error", failure.toString());
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
catch (Exception io) { failure.addSuppressed(io); }
SanctuaryMod.LOGGER.error("Transit29 native traversal failed", failure);
}
}
@@ -6,7 +6,7 @@
"environment": "*",
"license": "GPL-3.0-or-later",
"entrypoints": {
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-gametest": ["${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -207,8 +207,11 @@ public final class ExpansionStructures23 {
Structure original = choice.holder().value();
Entry entry = choice.entry();
boolean hut = entry.family().equals("swamp_hut");
boolean layeredMine = island.origin() && island.generation() == 24 && entry.family().equals("mineshaft");
List<Integer> mineHeights = null;
int variants = hut ? hutHeights.size() : entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
for (int vertical = 0; vertical < variants; vertical++) {
if (mineHeights != null && vertical >= mineHeights.size()) break;
checkDeadline(deadlineNanos);
int targetY = hut ? hutHeights.get(vertical) : undergroundY - vertical * 20;
ExpansionChunkGenerator nativeView = hut && view instanceof NativeView local
@@ -229,6 +232,16 @@ public final class ExpansionStructures23 {
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
}
if (!start.isValid()) { diagnostic(island, candidate, "native-refused", entry.id()); continue; }
if (layeredMine) {
if (mineHeights == null) {
mineHeights = MineshaftHeights29.heights(start, undergroundY, ground.level.getMinY(), ground.level.getMaxY(),
(px, pz) -> ground.at(px, pz).column);
if (DIAGNOSTICS) SanctuaryMod.LOGGER.info("Native29 mine layers: seed {} chunk {} preferred {} chosen {} native pieces {}",
island.seed(), candidate.chunk(), undergroundY, mineHeights, start.getPieces().size());
}
if (vertical >= mineHeights.size()) break;
targetY = mineHeights.get(vertical);
}
// Only buried portal variants need the alternative underground elevations.
if (vertical > 0 && entry.family().equals("ruined_portal") && !undergroundPortal(start)) break;
start = adjustNativeStart(original, start, entry, island, targetY, nativeGround);
@@ -322,19 +335,24 @@ public final class ExpansionStructures23 {
private static String supportRejection(ExpansionChunkGenerator host, ExpansionIsland island, Entry entry,
StructureStart start, GroundSampler ground) {
BoundingBox total = start.getBoundingBox();
boolean nativeMine = island.origin() && island.generation() == 24 && entry.family().equals("mineshaft");
List<BoundingBox> occupied = nativeMine ? MineshaftAdmission29.volumes(start, (x, z) -> ground.at(x, z).column) : null;
if (nativeMine && occupied == null) return "mineshaft-no-native-support";
if (island.origin() && entry.habitat() != Habitat.UNDERGROUND && !undergroundPortal(start)) {
int nearestX = Math.clamp(island.centerX(), total.minX(), total.maxX());
int nearestZ = Math.clamp(island.centerZ(), total.minZ(), total.maxZ());
if (Math.hypot((double) nearestX - island.centerX(), (double) nearestZ - island.centerZ()) < 56) return "natural-arrival-buffer";
}
if (total.minY() < ground.level.getMinY() + 3 || total.maxY() > ground.level.getMaxY() - 4) return "outside-height";
if (!inside(island, total.minX(), total.minZ(), 3) || !inside(island, total.maxX(), total.minZ(), 3)
|| !inside(island, total.minX(), total.maxZ(), 3) || !inside(island, total.maxX(), total.maxZ(), 3)) return "outside-island";
for (BoundingBox footprint : nativeMine ? occupied : List.of(total))
if (!inside(island, footprint.minX(), footprint.minZ(), 3) || !inside(island, footprint.maxX(), footprint.minZ(), 3)
|| !inside(island, footprint.minX(), footprint.maxZ(), 3) || !inside(island, footprint.maxX(), footprint.maxZ(), 3)) return "outside-island";
// Vanilla reference discovery scans eight chunks around the decorating chunk.
if (total.minX() < start.getChunkPos().getMinBlockX() - 128 || total.maxX() > start.getChunkPos().getMaxBlockX() + 128
|| total.minZ() < start.getChunkPos().getMinBlockZ() - 128 || total.maxZ() > start.getChunkPos().getMaxBlockZ() + 128) return "outside-native-reference-range";
if (host instanceof SanctuaryChunkGenerator city) {
if (city.lostCityPlan(island).intersects(total.minX(), total.minZ(), total.maxX(), total.maxZ())) return "occupied-city";
for (BoundingBox volume : nativeMine ? occupied : List.of(total))
if (city.lostCityPlan(island).intersects(volume.minX(), volume.minZ(), volume.maxX(), volume.maxZ())) return "occupied-city";
if (ground.localCity == null) {
var local = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
local.add(city.lostCityPlan(island), city.undergroundPlan16(island));
@@ -343,13 +361,23 @@ public final class ExpansionStructures23 {
if (reservation != null) local.addReservation(reservation);
ground.localCity = local.build();
}
if (ground.localCity.intersects(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsCityConstruction15(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsCityConstruction16(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)
|| city.intersectsExperimentalStructures(total.minX() - 2, total.minY() - 12, total.minZ() - 2, total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2)) return "reserved-secret";
var legacyProtection = new BoundingBox(total.minX() - 2, total.minY() - 12, total.minZ() - 2,
total.maxX() + 2, total.maxY() + 4, total.maxZ() + 2);
for (BoundingBox volume : nativeMine ? occupied : List.of(legacyProtection)) {
if (ground.localCity.intersects(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsCityConstruction15(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsCityConstruction16(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ())
|| city.intersectsExperimentalStructures(volume.minX(), volume.minY(), volume.minZ(), volume.maxX(), volume.maxY(), volume.maxZ()))
return "reserved-secret";
}
if (island.origin() && !entry.family().equals("swamp_hut")) {
var binding = city.starterContext();
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
if (nativeMine) {
var regions = fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(binding.generator(), binding.random(),
total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16);
for (var volume : occupied) for (var region : regions)
if (MineshaftAdmission29.hydrology(region, volume)) return "protected-hydrology";
} else if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
total.getCenter().atY(total.minY()), Math.max(total.getXSpan(), total.getZSpan()) / 2 + 3, total.getYSpan() + 16, 26)) return "protected-hydrology";
}
}
@@ -373,7 +401,12 @@ public final class ExpansionStructures23 {
BoundingBox box = piece.getBoundingBox();
if (entry.family().equals("mineshaft")
&& piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
if (!MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
if (!nativeMine && !MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
continue;
}
if (nativeMine && piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCrossing) {
String crossingRefusal = MineshaftCrossing29.rejection(piece, start.getPieces(), (x, z) -> ground.at(x, z).column, ground.level.getMaxY());
if (crossingRefusal != null) return crossingRefusal;
continue;
}
int low = Integer.MAX_VALUE, high = Integer.MIN_VALUE, supported = 0, sampled = 0;
@@ -404,7 +437,12 @@ public final class ExpansionStructures23 {
low = Math.min(low, column.top); high = Math.max(high, column.top);
if (high - low > (entry.family().endsWith("pyramid") ? 5 : 9)) return "surface-slope";
}
if (buriedPiece && supported < Math.ceil(sampled * (entry.family().equals("mineshaft") ? 0.65 : 0.85))) return "cavity-not-enclosed";
if (buriedPiece && supported < Math.ceil(sampled * (entry.family().equals("mineshaft") ? 0.65 : 0.85))) {
if (nativeMine && DIAGNOSTICS) SanctuaryMod.LOGGER.info(
"Native29 mine enclosure: seed {} chunk {} piece {} box {} supported {}/{} required {}",
island.seed(), start.getChunkPos(), piece.getClass().getSimpleName(), box, supported, sampled, (int) Math.ceil(sampled * 0.65));
return "cavity-not-enclosed";
}
if (!buriedPiece && !aquatic) {
siteMin = Math.min(siteMin, low); siteMax = Math.max(siteMax, high);
if (siteMax - siteMin > 20) return "site-slope";
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.expansion;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Occupied native volumes, including potential wooden feet and suspension chains. No connecting-room infill. */
public final class MineshaftAdmission29 {
private MineshaftAdmission29() {}
public static List<BoundingBox> volumes(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
var result = new ArrayList<BoundingBox>();
for (var piece : start.getPieces()) {
var box = piece.getBoundingBox();
result.add(body(box));
if (piece instanceof MineshaftPieces.MineShaftCorridor) {
var anchors = MineshaftSupport28.anchorVolumes(piece, columns);
if (anchors == null) return null;
result.addAll(anchors);
}
}
return List.copyOf(result);
}
public static BoundingBox body(BoundingBox box) {
return new BoundingBox(box.minX() - 2, box.minY() - 1, box.minZ() - 2,
box.maxX() + 2, box.maxY() + 2, box.maxZ() + 2);
}
public static boolean intersects(List<BoundingBox> volumes, BoundingBox other) {
return volumes.stream().anyMatch(box -> box.intersects(other));
}
/** Same protected beds, sources and flow margins as SanctuaryDecorationProtection, queried as a rectangle. */
public static boolean hydrology(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
var water = region.water(); var lava = region.lava();
for (int x = box.minX() - 1; x <= box.maxX() + 1; x++) for (int z = box.minZ() - 1; z <= box.maxZ() + 1; z++)
for (var cell : water.cellsAt(x, z))
if (box.minY() <= cell.carveTop() + 1 && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 2) return true;
for (var spring : water.springs()) {
var b = spring.flowBounds();
if (intersects(box, b.minX()-2,b.minY()-2,b.minZ()-2,b.maxX()+2,b.maxY()+2,b.maxZ()+2)) return true;
var p = spring.source();
if (intersects(box,p.x()-3,p.y()-3,p.z()-3,p.x()+3,p.y()+3,p.z()+3)) return true;
}
for (var b : lava.flowBounds())
if (intersects(box,b.minX()-8,b.minY()-8,b.minZ()-8,b.maxX()+8,b.maxY()+8,b.maxZ()+8)) return true;
for (var spill : water.spills()) {
var b = spill.flowBounds();
if (intersects(box,b.minX()-3,b.minY()-3,b.minZ()-3,b.maxX()+3,b.maxY()+3,b.maxZ()+3)) return true;
}
for (var p : lava.cells()) if (intersects(box,p.x()-2,p.y()-2,p.z()-2,p.x()+2,p.y()+2,p.z()+2)) return true;
for (var p : lava.supports()) if (intersects(box,p.x()-2,p.y()-2,p.z()-2,p.x()+2,p.y()+2,p.z()+2)) return true;
return false;
}
private static boolean intersects(BoundingBox b, int minX,int minY,int minZ,int maxX,int maxY,int maxZ) {
return b.minX()<=maxX && b.maxX()>=minX && b.minY()<=maxY && b.maxY()>=minY && b.minZ()<=maxZ && b.maxZ()>=minZ;
}
}
@@ -0,0 +1,51 @@
package fr.koka.sanctuary.expansion;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Native crossings have a plank floor; their optional interior pillars do not require a close rock roof. */
public final class MineshaftCrossing29 {
private MineshaftCrossing29() {}
public static String rejection(StructurePiece crossing, List<StructurePiece> pieces,
BiFunction<Integer, Integer, NoiseColumn> columns, int maxY) {
var box = crossing.getBoundingBox();
boolean needsCarriedConnection = false;
int floorY = box.minY() - 1;
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
var column = columns.apply(x, z); var floor = column.getBlock(floorY);
if (!floor.getFluidState().isEmpty()) return "mineshaft-crossing-wet-floor";
if (floor.isFaceSturdy(EmptyBlockGetter.INSTANCE, new BlockPos(x, floorY, z), Direction.UP)) continue;
needsCarriedConnection = true;
// StructurePiece.isInterior tests floorY+1 against OCEAN_FLOOR_WG. Only count
// opaque terrain above the crossing's excavated body, so its own carving cannot remove it.
boolean interior = false;
for (int y = box.maxY() + 1; y < maxY; y++) if (Heightmap.Types.OCEAN_FLOOR_WG.isOpaque().test(column.getBlock(y))) {
interior = true; break;
}
if (!interior) return "mineshaft-crossing-unplaceable-floor";
}
// A wholly natural floor already carries itself. A new plank bridge borrows an actual
// adjacent corridor's certified support, never a distant or merely overlapping box.
if (needsCarriedConnection && pieces.stream().noneMatch(piece -> piece instanceof MineshaftPieces.MineShaftCorridor
&& joins(box, piece) && MineshaftSupport28.anchorVolumes(piece, columns) != null))
return "mineshaft-crossing-no-carried-connection";
return null;
}
private static boolean joins(BoundingBox crossing, StructurePiece corridor) {
var box = corridor.getBoundingBox();
if (box.minY() != crossing.minY()) return false;
if (corridor.getOrientation().getAxis() == Direction.Axis.X)
return (box.maxX() + 1 == crossing.minX() || crossing.maxX() + 1 == box.minX())
&& Math.min(box.maxZ(), crossing.maxZ()) - Math.max(box.minZ(), crossing.minZ()) + 1 >= 3;
return (box.maxZ() + 1 == crossing.minZ() || crossing.maxZ() + 1 == box.minZ())
&& Math.min(box.maxX(), crossing.maxX()) - Math.max(box.minX(), crossing.minX()) + 1 >= 3;
}
}
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.expansion;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
/** Coarse vertical search in the actual native graph, before unchanged detailed admission. */
public final class MineshaftHeights29 {
public static final int MAX_COLUMNS = 32;
public record Probe(NoiseColumn column, int floorOffset, int roofOffset, boolean corridor) {}
private record Score(int y, int covered, int direct, int distance) {}
private MineshaftHeights29() {}
public static List<Integer> heights(StructureStart start, int preferred, int minY, int maxY,
BiFunction<Integer, Integer, NoiseColumn> columns) {
var pieces = start.getPieces(); var total = start.getBoundingBox();
int originY = (total.minY() + total.maxY()) / 2;
int count = Math.min(MAX_COLUMNS, pieces.size());
var probes = new ArrayList<Probe>(count);
for (int i = 0; i < count; i++) {
var piece = pieces.get(count == 1 ? 0 : i * (pieces.size() - 1) / (count - 1));
var box = piece.getBoundingBox(); var center = box.getCenter();
probes.add(new Probe(columns.apply(center.getX(), center.getZ()), box.minY() - 1 - originY,
box.maxY() + 1 - originY, piece instanceof MineshaftPieces.MineShaftCorridor));
}
return choose(probes, preferred, minY + 3 + originY - total.minY(), maxY - 4 - total.maxY() + originY);
}
/** At most96 sampled levels in a384-block world; three native trials, not three new graphs per level. */
public static List<Integer> choose(List<Probe> probes, int preferred, int minY, int maxY) {
var scores = new ArrayList<Score>();
for (int y = Math.floorDiv(minY + 3, 4) * 4; y <= maxY; y += 4) {
int covered = 0, direct = 0;
for (var probe : probes) {
int floor = y + probe.floorOffset(), roof = y + probe.roofOffset();
boolean floorMass = solid(probe.column(), floor), roofMass = solid(probe.column(), roof);
boolean supported = probe.corridor()
? floorMass || anchor(probe.column(), floor - 1, floor - 20, -1)
|| anchor(probe.column(), roof, floor + 50, 1)
: near(probe.column(), floor, 6) && near(probe.column(), roof, 6);
if (supported) covered++;
if (floorMass) direct++;
if (roofMass) direct++;
}
if (covered > 0) scores.add(new Score(y, covered, direct, Math.abs(y - preferred)));
}
scores.sort(Comparator.comparingInt(Score::covered).reversed()
.thenComparing(Comparator.comparingInt(Score::direct).reversed())
.thenComparingInt(Score::distance).thenComparingInt(Score::y));
var chosen = new ArrayList<Integer>(3);
for (var score : scores) {
if (chosen.stream().anyMatch(y -> Math.abs(y - score.y()) < 8)) continue;
chosen.add(score.y()); if (chosen.size() == 3) break;
}
return List.copyOf(chosen);
}
private static boolean solid(NoiseColumn column, int y) {
var state = column.getBlock(y); return state.isSolid() && state.getFluidState().isEmpty();
}
private static boolean near(NoiseColumn column, int y, int reach) {
for (int offset = -reach; offset <= reach; offset++) if (solid(column, y + offset)) return true;
return false;
}
private static boolean anchor(NoiseColumn column, int from, int to, int step) {
for (int y = from; step > 0 ? y <= to : y >= to; y += step) {
if (!column.getBlock(y).getFluidState().isEmpty()) return false;
if (solid(column, y)) return true;
}
return false;
}
}
@@ -8,25 +8,67 @@ import net.minecraft.world.level.levelgen.structure.StructurePiece;
public final class MineshaftSupport28 {
private MineshaftSupport28() {}
public static boolean anchored(StructurePiece piece, BiFunction<Integer, Integer, NoiseColumn> columns) {
return anchorVolumes(piece, columns, false) != null;
}
/** Protect the nearer usable native anchor; downward wins an equal-distance tie. */
public static java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> anchorVolumes(StructurePiece piece,
BiFunction<Integer, Integer, NoiseColumn> columns) {
return anchorVolumes(piece, columns, true);
}
private static java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> anchorVolumes(StructurePiece piece,
BiFunction<Integer, Integer, NoiseColumn> columns, boolean allowDirectFloor) {
var volumes = new java.util.ArrayList<net.minecraft.world.level.levelgen.structure.BoundingBox>();
var box = piece.getBoundingBox();
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? box.getZSpan() : box.getXSpan();
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) for (int x : new int[]{0, 2}) {
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) {
int sectionAnchors = volumes.size();
for (int x : new int[]{0, 2}) {
var pier = SwampHutPlacement28.worldPosition(piece, x, -1, z);
var column = columns.apply(pier.getX(), pier.getZ()); boolean anchor = false;
var column = columns.apply(pier.getX(), pier.getZ());
var floor = column.getBlock(pier.getY());
// Vanilla setPlanksBlock retains a sturdy natural floor. Only a floor plank subsequently
// invokes placeDoubleLowerOrUpperSupport, so a rock ledge needs no distant anchor.
if (allowDirectFloor && floor.getFluidState().isEmpty()
&& !floor.is(net.minecraft.world.level.block.Blocks.OAK_PLANKS)
&& !floor.is(net.minecraft.world.level.block.Blocks.DARK_OAK_PLANKS)
&& floor.isFaceSturdy(net.minecraft.world.level.EmptyBlockGetter.INSTANCE, pier, net.minecraft.core.Direction.UP)) {
volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier));
continue;
}
Integer below = null, above = null;
// Native wooden support searches at most20 below its floor plank.
for (int y = pier.getY() - 1; y >= pier.getY() - 20; y--) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
if (state.isSolid()) {
below = y; break;
}
}
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
if (!anchor) for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
if (state.isSolid()) {
above = y; break;
}
}
if (!anchor) return false;
if (below == null && above == null) {
if (!allowDirectFloor) return null;
// Vanilla simply omits this optional pillar/chain. The other side of the same
// transverse section can still carry the corridor; both ends must remain anchored.
continue;
}
if (below != null && (above == null || pier.getY() - below <= above - pier.getY()))
volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier.getX(), below, pier.getZ(), pier.getX(), pier.getY(), pier.getZ()));
else volumes.add(new net.minecraft.world.level.levelgen.structure.BoundingBox(pier.getX(), pier.getY(), pier.getZ(), pier.getX(), above, pier.getZ()));
}
if (volumes.size() == sectionAnchors) {
if (allowDirectFloor && Boolean.getBoolean("sanctuary.expansion.structureDiagnostics"))
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("Native29 mine unanchored section: piece {} facing {} localZ {} floorY {}",
box, piece.getOrientation(), z, box.minY() - 1);
return null;
}
}
return true;
return java.util.List.copyOf(volumes);
}
}
@@ -0,0 +1,48 @@
package fr.koka.sanctuary.worldgen;
import java.util.Arrays;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Broad geological provinces, sampled once in island-relative coordinates.
* The mask selects where the existing 3D rock field can rise; it never defines a summit shape. */
public final class BalancedRelief303 {
private static final int SIZE = 65;
private final float[] samples = new float[SIZE * SIZE];
private final double radius, low, high;
private final long seed;
public BalancedRelief303(DensitySampler noise, double radius, long seed) {
this.radius = radius; this.seed = seed;
float[] distribution = new float[SIZE * SIZE]; int count = 0;
for (int z = 0; z < SIZE; z++) for (int x = 0; x < SIZE; x++) {
float value = noise.sampleValue(SamplerContext.EMPTY_UNCACHED, x * 2 - 64, 87, z * 2 - 64);
samples[z * SIZE + x] = value;
if (Math.hypot(x - 32, z - 32) <= 26) distribution[count++] = value;
}
Arrays.sort(distribution, 0, count);
low = distribution[count / 2]; high = Math.max(low + .0001, distribution[(int)(count * .82)]);
}
public double province(int x, int z) {
double gx = Math.clamp((x / radius + 1) * 32, 0, 63.999999);
double gz = Math.clamp((z / radius + 1) * 32, 0, 63.999999);
int ix = (int)gx, iz = (int)gz; double fx = gx - ix, fz = gz - iz;
double n = lerp(fz, lerp(fx, samples[iz*SIZE+ix], samples[iz*SIZE+ix+1]),
lerp(fx, samples[(iz+1)*SIZE+ix], samples[(iz+1)*SIZE+ix+1]));
double distance = Math.hypot(x, z);
return smooth((n - low) / (high - low)) * smooth((distance - 48) / 40)
* (1 - smooth((distance / radius - .68) / .22))
* Relief30.influence(seed, x, z, radius);
}
/** Continuous, strictly increasing vertical domain: no bands, ledges or added fine noise. */
public static double sourceY(int y, double province) {
if (y <= 170 || province <= 0) return y;
double d = y - 170;
// The first 30 blocks blend into untouched lower caves with a matching first derivative.
return 170 + d / (1 + .95 * province * smooth(d / 30));
}
public static double ceilingErosion(int y) { return 2 * smooth((y - 340) / 36.0); }
private static double lerp(double t, double a, double b) { return a + t * (b - a); }
private static double smooth(double t) { t = Math.clamp(t, 0, 1); return t*t*(3-2*t); }
}
@@ -0,0 +1,61 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
import java.util.*;
/** Opens existing curved rivers and basins; never draws an additional river. */
public final class BankOutlets30 {
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
private BankOutlets30() {}
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
/** Cut only a short bank. Nothing is built beneath a fall and its landing is unrestricted. */
public static Plan openBanks(long seed,Plan water,Sampler sampler,int maximum) {
var cuts=new LinkedHashMap<Long,Cell>();var features=new ArrayList<Feature>();var spills=new ArrayList<Spill>();
var mouths=new ArrayList<Point>();
var kinds = new HashMap<Long,Kind>();
water.features().forEach(f -> kinds.put(f.id(), f.kind()));
var candidates=water.cells().stream().filter(Cell::hasWater)
.filter(c -> Set.of(Kind.RIVER, Kind.LAKE, Kind.POND).contains(kinds.get(c.featureId())))
.sorted(Comparator.<Cell>comparingInt(c -> kinds.get(c.featureId()) == Kind.RIVER ? 0 : 1)
.thenComparingLong(c->mix(seed^key(c.x(),c.z()))).thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
for(var wet:candidates) {
if(mouths.size()>=maximum) break;
if(mouths.stream().anyMatch(p->Math.hypot(p.x()-wet.x(),p.z()-wet.z())<48)) continue;
for(var d:DIR) {
var adjacent=water.cellAt(wet.x()+d[0],wet.z()+d[1]);
if(adjacent!=null && adjacent.hasWater()) continue;
int reach=0;
for(int n=1;n<=5;n++) {
int px=wet.x()+d[0]*n,pz=wet.z()+d[1]*n;
if(sampler.sample(px,wet.waterY(),pz)<=0 && sampler.sample(px,wet.waterY()-3,pz)<=0
&& sampler.sample(px,wet.waterY()-6,pz)<=0) {reach=n;break;}
}
if(reach==0) continue;
long id=0x310000000L+(mix(seed)&0xfffffffL)*16+mouths.size();var local=new LinkedHashMap<Long,Cell>();
boolean conflict=false;
for(int n=1;n<=reach;n++) for(int side=-1;side<=1;side++) {
int px=wet.x()+d[0]*n+d[1]*side,pz=wet.z()+d[1]*n-d[0]*side;
var old=water.cellAt(px,pz);
if(old!=null && old.hasWater()) {conflict=true;continue;}
local.put(key(px,pz),new Cell(px,pz,-1,wet.waterY()-1-n/3,wet.waterY()+2,ShoreMaterial.STONE,id,0));
}
if(conflict) continue;
// Keep writes local to the same regional owner as the existing water.
if(local.values().stream().anyMatch(c -> c.x() < -255 || c.x() > 254 || c.z() < -255 || c.z() > 254)) continue;
cuts.putAll(local);features.add(feature(id,Kind.OUTLET,local.values(),List.of()));mouths.add(new Point(wet.x(),wet.z()));
var source=new Position(wet.x(),wet.waterY(),wet.z());
var outlet=new Position(wet.x()+d[0]*reach,wet.waterY()-reach/3,wet.z()+d[1]*reach);
var bounds=new FlowBounds(Math.min(source.x(),outlet.x())-3,0,Math.min(source.z(),outlet.z())-3,
Math.max(source.x(),outlet.x())+3,wet.waterY()+3,Math.max(source.z(),outlet.z())+3);
spills.add(new Spill(id,wet.featureId(),-1,source,outlet,List.of(source,outlet),bounds));break;
}
}
return water.overlay30(PopulationHydrology.additional30(cuts,features,spills,0,0));
}
private static Feature feature(long id,Kind kind,Collection<Cell> cells,List<Point> path) {
int x0=Integer.MAX_VALUE,z0=x0,x1=Integer.MIN_VALUE,z1=x1,water=-1,count=0;
for(var c:cells){x0=Math.min(x0,c.x());z0=Math.min(z0,c.z());x1=Math.max(x1,c.x());z1=Math.max(z1,c.z());if(c.hasWater()){water=c.waterY();count++;}}
return new Feature(id,kind,water,path,count,new Bounds(x0,z0,x1,z1));
}
private static long mix(long n){n=(n^(n>>>30))*0xbf58476d1ce4e5b9L;n=(n^(n>>>27))*0x94d049bb133111ebL;return n^(n>>>31);}
}
@@ -0,0 +1,77 @@
package fr.koka.sanctuary.worldgen;
import java.lang.ref.WeakReference;
import net.minecraft.world.level.levelgen.densityfunction.*;
/** Removes detached components of at most 32 blocks in the locally retouched field.
* The flood search reads the unfiltered field and stops on its 33rd connected block.
* All coordinates are global: a chunk edge is never treated as the edge of a component. */
public final class DetachedRock302 implements DensitySampler {
private static final int MIN_BODY = 33;
private static final ThreadLocal<Cache> CACHE = ThreadLocal.withInitial(Cache::new);
private static final int[][] SIDES = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
private final DensitySampler input;
private final double radius;
private final long seed;
private final boolean broad;
public DetachedRock302(DensitySampler input, double radius, long seed) {
this(input, radius, seed, false);
}
public DetachedRock302(DensitySampler input, double radius, long seed, boolean broad) {
this.input = input; this.radius = radius; this.seed = seed; this.broad = broad;
}
private boolean affected(int x, int y, int z) {
if (broad) return y > 170 && y < 376 && Math.hypot(x, z) > 48 && Math.hypot(x,z) < radius + 32;
return SurfaceRelief302.weight(x,y,z,radius) * Relief30.influence(seed,x,z,radius) > 0;
}
@Override public float sampleValue(SamplerContext context,int x,int y,int z) {
if (!affected(x,y,z)) return input.sampleValue(context,x,y,z);
var cache=CACHE.get();cache.select(this,context);
int slot=cache.get(this,context,x,y,z);float value=cache.values[slot];
if(value<=0 || cache.status[slot]==2) return value;
if(cache.status[slot]==3) return -0.000001F;
int[] xs=new int[MIN_BODY],ys=new int[MIN_BODY],zs=new int[MIN_BODY];xs[0]=x;ys[0]=y;zs[0]=z;
int count=1;boolean attached=false;
search:for(int head=0;head<count;head++)for(var side:SIDES){
int nx=xs[head]+side[0],ny=ys[head]+side[1],nz=zs[head]+side[2];
boolean seen=false;
for(int i=0;i<count;i++)if(xs[i]==nx&&ys[i]==ny&&zs[i]==nz){seen=true;break;}
if(seen)continue;
int next=cache.get(this,context,nx,ny,nz);
if(cache.values[next]<=0 || cache.status[next]==3)continue;
if(cache.status[next]==2 || !affected(nx,ny,nz)){attached=true;break search;}
xs[count]=nx;ys[count]=ny;zs[count]=nz;
if(++count==MIN_BODY){attached=true;break search;}
}
for(int i=0;i<count;i++)cache.status[cache.get(this,context,xs[i],ys[i],zs[i])]=(byte)(attached?2:3);
return attached?value:-0.000001F;
}
@Override public void sampleVolume(SamplerContext context,DensityBuffer buffer,DensityVolume volume){
DensitySampler.sampleVolumeNaive(context,buffer,volume,this);
}
/** Bounded primitive cache; weak identities cannot retain a closed world or sample context. */
private static final class Cache {
private static final int SIZE=1<<16;
private WeakReference<DetachedRock302> owner=new WeakReference<>(null);
private WeakReference<SamplerContext> context=new WeakReference<>(null);
private final int[] xs=new int[SIZE],ys=new int[SIZE],zs=new int[SIZE],stamps=new int[SIZE];
private final float[] values=new float[SIZE];
private final byte[] status=new byte[SIZE];
private int generation;
void select(DetachedRock302 selected,SamplerContext selectedContext){
if(owner.get()==selected&&context.get()==selectedContext)return;
owner=new WeakReference<>(selected);context=new WeakReference<>(selectedContext);
if(++generation==0){java.util.Arrays.fill(stamps,0);generation=1;}
}
int get(DetachedRock302 sampler,SamplerContext sample,int x,int y,int z){
int h=x*0x9e3779b9^Integer.rotateLeft(y*0x85ebca6b,11)^Integer.rotateLeft(z*0xc2b2ae35,22);
int slot=(h^(h>>>16))&(SIZE-1);
if(stamps[slot]!=generation||xs[slot]!=x||ys[slot]!=y||zs[slot]!=z){
float value=sampler.input.sampleValue(sample,x,y,z);
xs[slot]=x;ys[slot]=y;zs[slot]=z;values[slot]=value;status[slot]=1;stamps[slot]=generation;
}
return slot;
}
}
}
@@ -31,7 +31,7 @@ public final class PopulationHydrology {
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER, TERRACE }
public enum Kind { POND, LAKE, RIVER, TERRACE, OUTLET }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
@@ -133,6 +133,23 @@ public final class PopulationHydrology {
public List<Feature> features() { return features; }
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
public boolean isOutletCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.OUTLET && f.id() == cell.featureId()); }
/** Add an island-wide channel or short bank cuts, retaining the deeper water terraces. */
public Plan overlay30(Plan other) {
var surface = new LinkedHashMap<Long, Cell>();
var lowerCells = new ArrayList<Cell>();
for (var cell : allCells) {
if (isTerraceCell(cell)) lowerCells.add(cell);
else surface.put(key(cell.x(), cell.z()), cell);
}
for (var cell : other.allCells) surface.put(key(cell.x(), cell.z()), cell);
var mergedFeatures = new LinkedHashMap<Long, Feature>();
features.forEach(f -> mergedFeatures.put(f.id(), f));
other.features.forEach(f -> mergedFeatures.put(f.id(), f));
var mergedSpills = new ArrayList<>(spills); mergedSpills.addAll(other.spills);
var mergedCells = new ArrayList<>(surface.values()); mergedCells.addAll(lowerCells);
return rebuild(mergedCells, List.copyOf(mergedFeatures.values()), springs, springFlow, terraces, mergedSpills);
}
public List<Terrace> terraces() { return terraces; }
public List<Spill> spills() { return spills; }
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
@@ -254,6 +271,10 @@ public final class PopulationHydrology {
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
static Plan additional30(Map<Long, Cell> cells, List<Feature> features, List<Spill> spills, int columns, int samples) {
return new Plan(cells, features, List.of(), Set.of(), columns, samples,
new PopulationTerraces.Result(List.of(), List.of(), List.of(), spills, Set.of()));
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
@@ -17,6 +17,12 @@ public final class PopulationHydrologyRuntime {
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
Collections.synchronizedMap(new WeakHashMap<>());
public static boolean current30(NoiseBasedChunkGenerator generator) {
var settings = generator.generatorSettings();
return settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_5"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_10"))
|| settings.is(fr.koka.sanctuary.SanctuaryMod.id("unified_20"));
}
private PopulationHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
@@ -47,6 +53,9 @@ public final class PopulationHydrologyRuntime {
});
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
var water = PopulationHydrology.create(seed, memo).admitted();
if (current30(generator)) {
water = BankOutlets30.openBanks(seed, water, memo, 6);
}
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
lava.translated(region.originX(), region.originZ()));
@@ -133,6 +142,7 @@ public final class PopulationHydrologyRuntime {
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
if (plan.isOutletCell(cell)) continue;
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
@@ -12,7 +12,8 @@ import net.minecraft.world.level.levelgen.densityfunction.*;
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) implements DensityFunction {
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
boolean terraces29, boolean relief30, boolean surface302, boolean balanced303) implements DensityFunction {
public static final MapCodec<PopulationIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(PopulationIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("distortion").forGetter(PopulationIslandDensity::distortion),
@@ -25,14 +26,26 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensityFunction.CODEC.optionalFieldOf("caves_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::caves28),
DensityFunction.CODEC.optionalFieldOf("ridges_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::ridges28),
DensityFunction.CODEC.optionalFieldOf("erosion_28", DensityFunctions.zero()).forGetter(PopulationIslandDensity::erosion28),
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28)
Codec.BOOL.optionalFieldOf("geology_28", false).forGetter(PopulationIslandDensity::geology28),
Codec.BOOL.optionalFieldOf("terraces_29", false).forGetter(PopulationIslandDensity::terraces29),
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30),
Codec.BOOL.optionalFieldOf("surface_302", false).forGetter(PopulationIslandDensity::surface302),
Codec.BOOL.optionalFieldOf("balanced_303", false).forGetter(PopulationIslandDensity::balanced303)
).apply(instance, PopulationIslandDensity::new));
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false, false, false, false, false);
}
public PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28) {
this(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail, capacity,
caves28, ridges28, erosion28, geology28, false, false, false, false);
}
@Override public DensitySampler compileSampler(CompileContext context) {
@@ -44,8 +57,9 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
var ridges = geology28 ? ridges28.compileSampler(context) : null;
var erosion = geology28 ? erosion28.compileSampler(context) : null;
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
var balanced = balanced303 ? new BalancedRelief303(coarse, capacity.radius(), seed) : null;
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
return new DensitySampler() {
DensitySampler field = new DensitySampler() {
private float terrainAt(SamplerContext sample, double x, double y, double z) {
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
double fx = x - ix, fy = y - iy, fz = z - iz;
@@ -62,20 +76,34 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
}
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
int localY = y - 64;
float coarseValue = coarse.sampleValue(sample, x, localY, z);
float fineValue = fine.sampleValue(sample, x, localY, z);
float terrainValue;
double province = balanced != null ? balanced.province(x, z) : 0;
double mappedY = balanced != null ? BalancedRelief303.sourceY(y, province) : y;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
if (weight > 0) {
var shift = CavesRelief28.warp(weight,
if (balanced != null) weight *= province;
else if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
double surfaceWeight = surface302 ? SurfaceRelief302.weight(x, y, z, capacity.radius())
* Relief30.influence(seed, x, z, capacity.radius()) : 0;
if (weight > 0 || surfaceWeight > 0 || mappedY != y) {
var shift = weight > 0 ? CavesRelief28.warp(weight,
erosion.sampleValue(sample, x + 173, y - 59, z - 89),
ridges.sampleValue(sample, x, y, z),
erosion.sampleValue(sample, x - 211, y + 137, z + 307));
terrainValue = terrainAt(sample, x - shift.x(), y - 64 - shift.y(), z - shift.z());
erosion.sampleValue(sample, x - 211, y + 137, z + 307)) : new CavesRelief28.Warp(0,0,0);
double sourceY = mappedY - 64 - shift.y();
if (surfaceWeight > 0) sourceY = SurfaceRelief302.sourceY(sourceY,surfaceWeight,
coarse.sampleValue(sample,x-233,y+419,z+157));
if (terraces29) sourceY = Terraces29.sourceY(sourceY, coarseValue, fineValue,
Terraces29.weight(y, weight));
terrainValue = terrainAt(sample, x - shift.x(), sourceY, z - shift.z());
} else terrainValue = raw.sampleValue(sample, x, y - 64, z);
// Keep the island envelope at actual coordinates, independent from displaced rock.
int localY = y - 64;
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
edge.sampleValue(sample, x, 0, z), coarseValue,
fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity, mappedY - 64);
if (balanced != null) natural = Math.max(-1, natural - (float) BalancedRelief303.ceilingErosion(y));
if (natural <= 0) return natural;
var region = capacity.regionAt(x, z);
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
@@ -94,12 +122,22 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
}
};
return surface302 || balanced303 ? new DetachedRock302(field,capacity.radius(),seed, balanced303) : field;
}
/** Native diagnostics can compile the exact accepted27 field without new adaptations. */
public PopulationIslandDensity legacy27() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, DensityFunctions.zero(), DensityFunctions.zero(), DensityFunctions.zero(), false);
}
/** Exact accepted28 deformation and caves, without the fine local ledges. */
public PopulationIslandDensity legacy28() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, false, false, false, false);
}
public PopulationIslandDensity legacy301() {
return new PopulationIslandDensity(terrain, distortion, sculpt, detail, underside, riftWarp, riftDetail,
capacity, caves28, ridges28, erosion28, geology28, terraces29, relief30, false, false);
}
public static long regionSeed(long seed, IslandCapacity.Region region) {
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
@@ -109,7 +147,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new PopulationIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28);
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30, surface302, balanced303);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -7,6 +7,12 @@ public final class PopulationIslandShape {
/** Generation v3. The natural field and lateral sculpture are sampled in their original frame. */
public static float raisedDensity(int x, int y, int z, float terrain, float distortion,
float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity) {
return raisedDensity(x, y, z, terrain, distortion, sculpt, detail, underside, offset, height, capacity, (double)y - offset);
}
/** A raised province relaxes only the old upper fade; lateral and lower coast cuts stay in place. */
public static float raisedDensity(int x, int y, int z, float terrain, float distortion,
float sculpt, float detail, float underside, int offset, int height, IslandCapacity capacity, double upperLocalY) {
double distance = Math.hypot((double) x, (double) z);
if (distance >= capacity.terrainLimit() || y <= 0 || y >= height) return -1.0F;
double localY = (double) y - offset;
@@ -21,7 +27,7 @@ public final class PopulationIslandShape {
erosion += 2.0 * lower * lower * lower * lower;
// Upper silhouette is exactly the old one translated by offset. Absolute-world fades are
// safety margins, outside the normal terrain, rather than the island's visible underside.
erosion += 2.0 * smoothstep((localY - 224.0) / 32.0);
erosion += 2.0 * smoothstep((upperLocalY - 224.0) / 32.0);
erosion += 2.0 * smoothstep((24.0 - y) / 24.0);
erosion += 2.0 * smoothstep((y - (height - 24.0)) / 24.0);
return bounded(terrain, erosion);
@@ -0,0 +1,15 @@
package fr.koka.sanctuary.worldgen;
/** Alpha30.1 restores the global signed warp, except for one narrow natural plateau area. */
public final class Relief30 {
private Relief30() {}
public static double influence(long seed, int x, int z, double radius) {
// The earlier plateau field remains along one candidate transit alignment.
// Nothing is filled or levelled: the raw field and all existing caves remain active.
double along = x / (radius * .62);
double across = (z + radius * .25) / Math.max(20, radius * .08);
double distance = Math.hypot(along, across);
double t = Math.clamp((distance - 1) / .45, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -109,6 +109,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
if (current == null || heightAccessor == null) throw new IllegalStateException("Aerial sampling before world attachment");
return current.generator().getBaseColumn(x, z, heightAccessor, current.random());
}
public boolean aerialDryWitness30(int x, int groundY, int z) {
var current = originContext;
if (current == null) throw new IllegalStateException("Aerial sampling before world attachment");
for (var cell : PopulationHydrologyRuntime.cellsAt(current.generator(), current.random(), x, z))
if (groundY >= cell.bedY() && groundY <= Math.max(cell.carveTop(), cell.waterY()) + 1) return false;
return true;
}
private volatile fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23 mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlan23.EMPTY;
private boolean mineralPrepared23;
@@ -344,11 +351,13 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
heightAccessor, pos -> protectsCore22(pos) || protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|| ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ()) || heritagePlan19.protects(pos.getX(), pos.getY(), pos.getZ()));
long start = System.nanoTime();
transitPlan21 = generation >= 22
transitPlan21 = generation == 24
? fr.koka.sanctuary.worldgen.transit.TransitPlanner29.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: generation >= 22
? fr.koka.sanctuary.worldgen.transit.TransitPlanner22.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled)
: fr.koka.sanctuary.worldgen.transit.TransitPlanner21.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
int directColumns = 0;
if ((generation == 22 || (generation == 23 || generation == 24)) && transitPlan21.isEmpty()) {
if ((generation == 22 || generation == 23) && transitPlan21.isEmpty()) {
directColumns = sampled.columnsUsed();
sampled = new fr.koka.sanctuary.worldgen.transit.TransitTerrain21(root, terrain.generator(), terrain.random(),
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
@@ -397,7 +406,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain21(root, terrain.generator(), terrain.random(), heightAccessor,
pos -> protectsDecoration(pos) || protectsCore22(pos));
long started = System.nanoTime();
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
mineralSanctuary23 = fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.plan30(root.seed(), root.centerX(), root.centerZ(), root.diameter(),
new fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryPlanner23.Terrain() {
@Override public boolean owns(int x, int z) { return sampled.owns(x, z); }
@Override public int surfaceY(int x, int z) { return sampled.ground(x, z).top(); }
@@ -0,0 +1,31 @@
package fr.koka.sanctuary.worldgen;
/** Locally phased ledges in the existing rock field, with no shared horizontal level. */
public final class SurfaceRelief302 {
private SurfaceRelief302() {}
public static double weight(int x, int y, int z, double radius) {
double distance = Math.hypot((double) x, z);
return smooth((y - 140) / 48.0) * (1 - smooth((y - 316) / 36.0))
* smooth((distance - 48) / 32.0)
* (1 - smooth((distance / radius - .86) / .12));
}
/** Coherent patches move together; narrow, continuous transitions form local ledges.
* No rounding of Y, shared layer height or per-block random displacement. */
public static double sourceY(double originalY, double weight, float noise) {
if(weight<=0)return originalY;
double period=6.5+1.5*Math.tanh(noise*1.5);
double phase=11*Math.tanh(noise);
double band=(originalY+phase)/period;
double fraction=band-Math.floor(band);
double ledge=smooth((fraction-.30)/.40);
double offset=Math.clamp((ledge-fraction)*period,-2.25,2.25);
return originalY+offset*weight;
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.worldgen;
/** Small, locally phased ledges on the accepted three-dimensional deformation. */
public final class Terraces29 {
public static final double MAX_OFFSET = 2.5;
public static final int START_Y = 214;
private Terraces29() {}
public static double weight(int y, double deformationWeight) {
return smooth((y - START_Y) / 30.0) * deformationWeight;
}
/** Period and phase both follow 3D noise; there is no shared horizontal layer grid. */
public static double sourceY(double originalY, float coarse, float detail, double weight) {
if (weight <= 0) return originalY;
double period = 7.5 + 1.5 * Math.tanh(coarse * .9);
double phase = 7 * Math.tanh(coarse * .7) + 2 * Math.tanh(detail * .65);
double band = (originalY + phase) / period;
double fraction = band - Math.floor(band);
double shaped = smooth((fraction - .3) / .4);
double offset = Math.clamp((shaped - fraction) * period * .9, -MAX_OFFSET, MAX_OFFSET);
return originalY + offset * Math.clamp(weight, 0, 1);
}
private static double smooth(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
}
@@ -14,7 +14,7 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
/** Fixed work limits, not wall-clock-dependent draws. No chunk is requested or modified by this planner. */
public final class AerialPlanner24 {
public static final int MAX_ATTEMPTS_PER_SITE = 64;
public static final int MAX_COLUMNS = 8192;
public static final int MAX_COLUMNS = 16384;
public static final int ARRIVAL_RADIUS = 96;
public static final int MIN_ISLET_GAP = 16;
private AerialPlanner24() {}
@@ -22,6 +22,8 @@ public final class AerialPlanner24 {
NoiseColumn column(int x, int z);
/** Planned structures may add rock above the original noise column or cut its support. */
default boolean reserved(int x, int z, int minY, int maxY) { return false; }
/** The raw noise surface may subsequently be cut into a pond or an open river bank. */
default boolean dryWitness(int x, int groundY, int z) { return true; }
}
public static AerialPlan24 forWorld(ServerLevel level, SanctuaryChunkGenerator generator) {
@@ -30,6 +32,9 @@ public final class AerialPlanner24 {
generator.prepareAerialSampling24(level);
return plan(level.getSeed(), generator.initialDiameter(), new Terrain() {
@Override public NoiseColumn column(int x, int z) { return generator.aerialBaseColumn24(x, z); }
@Override public boolean dryWitness(int x, int groundY, int z) {
return generator.aerialDryWitness30(x, groundY, z);
}
@Override public boolean reserved(int x, int z, int minY, int maxY) {
return generator.intersectsExperimentalStructures(x, minY, z, x, maxY, z);
}
@@ -53,11 +58,15 @@ public final class AerialPlanner24 {
int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
candidates: for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
probe.candidate = ids[ordinal] + " attempt=" + attempt;
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
// Keep the four remaining bearings and seeded shapes; the former first sector is simply free.
double angle = initialAngle + (ordinal + 1) * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
+ (attempt / 16) * .41;
// After the initial sector, cover the whole island instead of repeatedly inspecting
// the same mountain. First-choice locations and all geometry rules remain intact.
if (!merchant && attempt >= 4) angle = initialAngle + (attempt - 4) * 2.399963229728653;
int facing = (int) Math.floorMod(mix(draw), 4);
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
Shore shore;
@@ -80,12 +89,18 @@ public final class AerialPlanner24 {
cx = (int) Math.round(Math.cos(angle) * distance);
cz = (int) Math.round(Math.sin(angle) * distance);
int top = probe.surface(cx, cz);
if (top < 120 || probe.terrain.reserved(cx, cz, top, top + 3)) continue;
shore = new Shore(cx, top + 1, cz);
if (top < 120 || top + MIN_ISLET_GAP + 1 + height > 350
|| probe.terrain.reserved(cx, cz, top, top + 3)) continue;
shore = dryViewpoint(probe, cx, cz);
if (shore == null) continue;
int highest = top;
for (int ox = -worldWidth / 2; ox <= worldWidth / 2; ox += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4)
for (int oz = -worldDepth / 2; oz <= worldDepth / 2; oz += 4) {
highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
// One obstruction is enough to refuse a candidate. Do not consume the
// remaining column budget on an island that already cannot fit vertically.
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
}
bottom = highest + MIN_ISLET_GAP + 1;
}
if (bottom < 32 || bottom + height > 350) continue;
@@ -98,19 +113,32 @@ public final class AerialPlanner24 {
} else {
// Certify every physical rock column, not only the coarse samples used to
// choose a height. This may raise the whole unchanged island, never the ground.
// A coarse footprint rejects candidates mostly over holes before the full
// per-block certification. Matching the final75% rule avoids exhausting the
// shared budget on the same recessed coast in a small island.
int coarseCount = 0, coarseSupported = 0;
for (int x = 0; x < width; x += 4) for (int z = 0; z < depth; z += 4) {
if (!isletColumn(site, x, z)) continue;
coarseCount++;
if (probe.surface(site.worldX(x,z),site.worldZ(x,z)) >= 120) coarseSupported++;
}
if (coarseSupported * 4 < coarseCount * 3) continue;
int highest = 0, supported = 0, count = 0;
for (int x = 0; x < width; x++) for (int z = 0; z < depth; z++) {
if (!isletColumn(site, x, z)) continue;
int wx = site.worldX(x, z), wz = site.worldZ(x, z);
highest = Math.max(highest, probe.highest(wx, wz));
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
if (probe.surface(wx, wz) >= 120) supported++;
count++;
}
if (supported * 4 < count * 3) continue;
if (site.waterfall()) {
var outlet = site.waterfallClaimBounds();
for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++)
highest = Math.max(highest, probe.highest(x, z));
for (int x = outlet.minX(); x <= outlet.maxX(); x++) for (int z = outlet.minZ(); z <= outlet.maxZ(); z++) {
highest = Math.max(highest, probe.highest(x, z));
if (highest + MIN_ISLET_GAP + 1 + height > 350) continue candidates;
}
}
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
if (bottom + height > 350) continue;
@@ -196,15 +224,29 @@ public final class AerialPlanner24 {
return true;
}
private record Shore(int x, int y, int z) {}
private static Shore dryViewpoint(Probe probe, int x, int z) {
// Keep the site above the same terrain. Move only its saved walking viewpoint
// to a nearby dry bank when the centre is subsequently carved by hydrology.
for (int r = 0; r <= 12; r += 2) for (int dx = -r; dx <= r; dx += 2)
for (int dz = -r; dz <= r; dz += 2) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
int top = probe.surface(x + dx, z + dz);
if (top >= 120 && probe.terrain.dryWitness(x + dx, top, z + dz)
&& !probe.terrain.reserved(x + dx, z + dz, top, top + 3))
return new Shore(x + dx, top + 1, z + dz);
}
return null;
}
private static final class Probe {
final Terrain terrain; final int radius;
String candidate = "unassigned";
final Map<Long, NoiseColumn> columns = new HashMap<>();
Probe(Terrain terrain, int radius) { this.terrain = terrain; this.radius = radius; }
NoiseColumn column(int x, int z) {
if (Math.hypot(x, z) >= radius) return null;
long key = ((long) x << 32) ^ (z & 0xffffffffL);
var present = columns.get(key); if (present != null) return present;
if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted");
if (columns.size() >= MAX_COLUMNS) throw new IllegalStateException("aerial_sites_unavailable: terrain column budget exhausted at " + candidate);
var result = terrain.column(x, z);
if (result == null) throw new IllegalStateException("Aerial terrain query returned no column");
columns.put(key, result); return result;
@@ -5,7 +5,7 @@ import java.util.Map;
/** Finite terrain search: at most48 horizontal candidates, four orientations, three depths. */
public final class MineralSanctuaryPlanner23 {
public static final int MAX_COLUMNS = 10_000, MAX_SAMPLES = 1_200_000;
public static final int MAX_COLUMNS = 24_000, MAX_SAMPLES = 2_400_000;
private MineralSanctuaryPlanner23() {}
public interface Terrain {
boolean owns(int x, int z);
@@ -14,12 +14,19 @@ public final class MineralSanctuaryPlanner23 {
boolean protectedAt(int x, int y, int z);
}
public static MineralSanctuaryPlan23 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, false);
}
public static MineralSanctuaryPlan23 plan30(long seed, int cx, int cz, int diameter, Terrain terrain) {
return search(seed, cx, cz, diameter, terrain, true);
}
private static MineralSanctuaryPlan23 search(long seed, int cx, int cz, int diameter, Terrain terrain, boolean exposedFallback) {
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IllegalArgumentException("Unsupported mineral island size");
var p = new Probe(terrain);
try {
for (int attempt = 0; attempt < 48; attempt++) {
for (int attempt = 0; attempt < (exposedFallback ? 96 : 48); attempt++) {
double angle = unit(mix(seed ^ 0x23CA7EL)) * Math.PI * 2 + attempt * 2.399963229728653;
double radius = diameter * (.17 + .045 * (attempt % 4));
p.exposed = attempt >= 48;
double radius = diameter * (p.exposed ? .27 + .04 * (attempt % 5) : .17 + .045 * (attempt % 4));
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
int surface = p.surface(x, z);
if (surface < 80) { p.reject("candidate-surface"); continue; }
@@ -39,7 +46,7 @@ public final class MineralSanctuaryPlanner23 {
var result = new MineralSanctuaryPlan23(c, p.samples, p.columns.size(), p.rejections);
System.getLogger(MineralSanctuaryPlanner23.class.getName()).log(System.Logger.Level.INFO,
"Mineral23 accepted=" + (c != null) + " columns=" + p.columns.size() + " samples=" + p.samples
+ " chamber=" + c + " rejected=" + p.rejections);
+ " exposed=" + p.exposed + " chamber=" + c + " rejected=" + p.rejections);
return result;
}
/** The fissure ends in existing traversable air (a cave or the exposed island side), not sealed rock. */
@@ -81,9 +88,9 @@ public final class MineralSanctuaryPlanner23 {
tested++;
}
}
if (columns == 0 || covered * 5 < columns * 3 || coveredQuadrants != 15)
if (columns == 0 || covered * 5 < columns * (p.exposed ? 1 : 3) || (p.exposed ? Integer.bitCount(coveredQuadrants) < 2 : coveredQuadrants != 15))
return p.reject("coarse-cover-distribution");
return tested > 0 && rock * 5 >= tested * 3 || p.reject("coarse-rock");
return tested > 0 && rock * 5 >= tested * (p.exposed ? 1 : 3) || p.reject("coarse-rock");
}
/** Reject unsupported candidates using a small shared column lattice before reading the full floor. */
private static boolean coarseBearings(MineralSanctuaryPlan23.Chamber c, Probe p) {
@@ -98,12 +105,12 @@ public final class MineralSanctuaryPlanner23 {
else gaps.add(point);
}
// Sampling is deliberately permissive about the proportion; dense certification still requires60%.
if (anchors.size() < gaps.size()) return p.reject("coarse-anchor-majority");
if (anchors.size() * (p.exposed ? 2 : 1) < gaps.size()) return p.reject("coarse-anchor-majority");
for (var gap : gaps) {
boolean supported = false;
for (var anchor : anchors) {
int dx = gap[0] - anchor[0], dz = gap[1] - anchor[1];
if (dx * dx + dz * dz <= 100) { supported = true; break; }
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100)) { supported = true; break; }
}
if (!supported) return p.reject("coarse-anchor-distance");
}
@@ -137,14 +144,14 @@ public final class MineralSanctuaryPlanner23 {
anchored[x][z] = true; direct++;
}
}
if (columns == 0 || direct * 5 < columns * 3) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * 3) return p.reject("full-cover-distribution");
if (columns == 0 || direct * 5 < columns * (p.exposed ? 2 : 3)) return p.reject("floor-anchor-majority");
if (covered * 5 < columns * (p.exposed ? 1 : 3)) return p.reject("full-cover-distribution");
for (int z = 0; z < c.depth(); z++) for (int x = 0; x < c.width(); x++) {
if (!footprint[x][z] || anchored[x][z]) continue;
boolean nearby = false;
for (int dz = -10; dz <= 10 && !nearby; dz++) for (int dx = -10; dx <= 10; dx++) {
for (int dz = p.exposed ? -18 : -10; dz <= (p.exposed ? 18 : 10) && !nearby; dz++) for (int dx = p.exposed ? -18 : -10; dx <= (p.exposed ? 18 : 10); dx++) {
int ax = x + dx, az = z + dz;
if (dx * dx + dz * dz <= 100 && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
if (dx * dx + dz * dz <= (p.exposed ? 324 : 100) && ax >= 0 && ax < c.width() && az >= 0 && az < c.depth()
&& anchored[ax][az]) { nearby = true; break; }
}
if (!nearby) return p.reject("floor-anchor-distance");
@@ -158,7 +165,7 @@ public final class MineralSanctuaryPlanner23 {
}
private static final class Probe {
final Terrain terrain; final Map<Long, Integer> columns = new HashMap<>();
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples;
final Map<String, Integer> rejections = new java.util.TreeMap<>(); int samples; boolean exposed;
Probe(Terrain terrain) { this.terrain = java.util.Objects.requireNonNull(terrain); }
int surface(int x, int z) {
if (!terrain.owns(x, z)) return -1;
@@ -50,6 +50,9 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
}
public boolean protects(int x, int y, int z) {
if (contains(x, y, z)) return true;
// Intact neighboring rock can carry the continuous masonry walls and roof.
if (x >= minX - 1 && x <= maxX() + 1 && z >= minZ - 1 && z <= maxZ() + 1
&& y >= minY && y <= maxY() + 2) return true;
if (kind == Kind.ACCESS && x >= centerX() - 2 && x <= centerX() + 2
&& z >= minZ - 3 && z < minZ && y >= surfaceY() - 2 && y <= surfaceY() + 3) return true;
// Two untouched layers carry the slab and the certified short piers.
@@ -78,8 +81,8 @@ public record TransitPlan21(List<Module> modules, int samples, String diagnostic
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
for (var m : modules) if (m.kind() == Kind.ACCESS && x0 <= m.centerX() + 2 && x1 >= m.centerX() - 2
&& z0 < m.minZ() && z1 >= m.minZ() - 3 && y0 <= m.surfaceY() + 3 && y1 >= m.surfaceY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() && x1 >= m.minX() && z0 <= m.maxZ() && z1 >= m.minZ()
&& y0 <= m.maxY() && y1 >= m.boundsMinY() - 2) return true;
for (var m : modules) if (x0 <= m.maxX() + 1 && x1 >= m.minX() - 1 && z0 <= m.maxZ() + 1 && z1 >= m.minZ() - 1
&& y0 <= m.maxY() + 2 && y1 >= m.boundsMinY() - 2) return true;
return false;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
@@ -0,0 +1,194 @@
package fr.koka.sanctuary.worldgen.transit;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Kind;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Module;
import fr.koka.sanctuary.worldgen.transit.TransitPlan21.Support;
import fr.koka.sanctuary.worldgen.transit.TransitPlanner21.Terrain;
import java.util.*;
/** The established road and stair shafts, fitted from their entrances before their depth.
* Only the indispensable traversal uses these civil-engineering bearings. No terrain is added. */
public final class TransitPlanner29 {
public static final int MAX_COLUMNS = 12_000;
private TransitPlanner29() {}
public static TransitPlan21 plan(long seed, int cx, int cz, int diameter, Terrain terrain) {
if (!List.of(512, 724, 1024).contains(diameter)) throw new IllegalArgumentException("Unsupported transit diameter");
var p = new Probe(terrain); int layouts = 0; String reason = "no natural entrance pair";
var entries = new ArrayList<Alignment>();
int mainRadius = Math.max(68, (int) Math.round(diameter * .19 / 17) * 17);
var radii = new LinkedHashSet<Integer>();
for (int radius : new int[]{mainRadius, mainRadius - 17, mainRadius - 34, mainRadius + 17})
if (radius * 2 >= diameter / 4) radii.add(radius);
try {
// Cheap, cached entrance scans precede any expensive underground volume scan.
search: for (int advance : new int[]{0, -17, 17, -34, 34, -51, 51})
for (int radius : radii) for (boolean alongX : new boolean[]{true, false})
for (int shift : new int[]{-diameter / 8, diameter / 8, -diameter / 16, diameter / 16, 0,
-3 * diameter / 16, 3 * diameter / 16}) {
if (++layouts > 320 || p.columns.size() >= 3_000) break search;
int ax = cx + (alongX ? advance - radius : shift), az = cz + (alongX ? shift : advance - radius);
int bx = cx + (alongX ? advance + radius : shift), bz = cz + (alongX ? shift : advance + radius);
int a = entrance(ax, az, p), b = entrance(bx, bz, p);
if (a < 0 || b < 0 || Math.abs(a - b) > 95) continue;
entries.add(new Alignment(ax, az, bx, bz, a, b, alongX));
if (entries.size() >= 48) break search;
}
// Preserve every complete route already accepted by the primary search. Only after
// its failure may an incompatible optional footing be omitted, using other bearings.
for (boolean selectiveFootings : new boolean[]{false, true}) {
p.selectiveFootings = selectiveFootings;
for (var a : entries) {
var floors = new LinkedHashSet<Integer>(); int low = Math.min(a.topA, a.topB), high = Math.max(a.topA, a.topB);
for (int floor : new int[]{low - 26, low - 42, low - 58, low - 74, 200, 184, 160})
if (floor >= 40 && floor <= low - 18 && floor >= high - 140) floors.add(floor);
for (int floor : floors) {
var modules = route(seed, a, floor, p, false);
if (modules == null) { reason = p.reason; continue; }
var complete = new ArrayList<Module>();
for (var module : modules) {
var certified = certify(module, p, true);
if (certified == null) { reason = p.reason; break; }
complete.add(certified);
}
if (complete.size() != modules.size()) continue;
addHall(complete, floor, a.alongX, seed, p);
return new TransitPlan21(complete, p.samples, "accepted29 axis=" + (a.alongX ? "east-west" : "north-south")
+ " floor=" + floor + " span=" + Math.max(Math.abs(a.bx - a.ax), Math.abs(a.bz - a.az))
+ " entries=" + entries.size() + " layouts=" + layouts + " columns=" + p.columns.size()
+ (selectiveFootings ? " selective-footings" : ""));
}
}
}
} catch (Budget exhausted) { reason = "bounded29 search exhausted: " + p.reason; }
return new TransitPlan21(List.of(), p.samples, reason + "; entries=" + entries.size() + "; layouts=" + layouts + "; columns=" + p.columns.size());
}
/** A three-wide doorstep is sufficient; the surrounding hillside need not be a flat platform. */
private static int entrance(int cx, int cz, Probe p) {
long key = key(cx, cz); Integer known = p.entrances.get(key); if (known != null) return known;
int low = 384, high = -1;
for (int x = cx - 1; x <= cx + 1; x++) {
int y = p.surface(x, cz - 9); low = Math.min(low, y); high = Math.max(high, y);
}
boolean valid = low >= 65 && high <= 360 && high - low <= 1;
if (valid) for (int x = cx - 1; x <= cx + 1; x++) {
int y = p.surface(x, cz - 9);
if (p.protectedAt(x, y, cz - 9) || p.protectedAt(x, high + 1, cz - 9)
|| p.protectedAt(x, high + 2, cz - 9)) valid = false;
}
p.entrances.put(key, valid ? high : -1); return valid ? high : -1;
}
private record Alignment(int ax, int az, int bx, int bz, int topA, int topB, boolean alongX) {}
private static void addHall(List<Module> route, int floor, boolean alongX, long seed, Probe p) {
int index = 1 + (route.size() - 2) / 2; var junction = route.get(index);
for (int side : new int[]{-1, 1}) {
try {
int x = alongX ? junction.centerX() - 4 : side < 0 ? junction.minX() - 17 : junction.maxX() + 1;
int z = alongX ? side < 0 ? junction.minZ() - 17 : junction.maxZ() + 1 : junction.centerZ() - 4;
var passage = certify(new Module(Kind.GALLERY, x, floor - 1, z, alongX ? 9 : 17, 10,
alongX ? 17 : 9, alongX ? 0 : 1, alongX ? 5 : 10, seed ^ 0x29B1L ^ side, List.of()), p, true);
if (passage == null || !p.anchored.getOrDefault(passage, false)) continue;
int hx = alongX ? junction.centerX() - 8 : side < 0 ? passage.minX() - 21 : passage.maxX() + 1;
int hz = alongX ? side < 0 ? passage.minZ() - 21 : passage.maxZ() + 1 : junction.centerZ() - 8;
int opening = alongX ? side < 0 ? 4 : 1 : side < 0 ? 2 : 8;
var hall = certify(new Module(Kind.HALL, hx, floor - 1, hz, alongX ? 17 : 21, 10,
alongX ? 21 : 17, alongX ? 0 : 1, opening, seed ^ 0x29A1L ^ side, List.of()), p, true);
if (hall == null) continue;
int branch = alongX ? side < 0 ? 1 : 4 : side < 0 ? 8 : 2;
var joined = certify(new Module(Kind.JUNCTION, junction.minX(), junction.minY(), junction.minZ(),
junction.width(), 10, junction.depth(), junction.facing(), junction.openings() | branch,
junction.seed(), junction.supports()), p, true);
if (joined == null) continue;
route.set(index, joined);
route.add(passage); route.add(hall); return;
} catch (Budget exhausted) { return; }
}
}
private static List<Module> route(long seed, Alignment a, int floor, Probe p, boolean full) {
var result = new ArrayList<Module>();
var first = certify(new Module(Kind.ACCESS, a.ax - 8, floor - 1, a.az - 8, 17, a.topA - floor + 6,
17, 0, a.alongX ? 2 : 4, seed ^ 0x2901L, List.of()), p, full);
if (first == null) return null;
var last = certify(new Module(Kind.ACCESS, a.bx - 8, floor - 1, a.bz - 8, 17, a.topB - floor + 6,
17, 0, a.alongX ? 8 : 1, seed ^ 0x2902L, List.of()), p, full);
if (last == null) return null;
result.add(first); int length = Math.max(Math.abs(a.bx - a.ax), Math.abs(a.bz - a.az)), weak = 0;
for (int d = 17; d < length; d += 17) {
int x = a.ax + (a.alongX ? d : 0), z = a.az + (a.alongX ? 0 : d);
var m = certify(new Module(Kind.GALLERY, x - (a.alongX ? 8 : 4), floor - 1, z - (a.alongX ? 4 : 8),
a.alongX ? 17 : 9, 10, a.alongX ? 9 : 17, a.alongX ? 1 : 0, a.alongX ? 10 : 5,
seed ^ (d * 0x9E3779B97F4A7C15L), List.of()), p, full);
if (m == null) return null;
weak = p.anchored.getOrDefault(m, false) ? 0 : weak + 1;
if (weak > 2) { p.reason = "unsupported span exceeds34 blocks"; return null; }
result.add(m);
}
result.add(last); return result;
}
private static Module certify(Module m, Probe p, boolean full) {
var supports = new ArrayList<Support>(); int quadrants = 0;
for (int x : new int[]{m.minX() + 1, m.centerX(), m.maxX() - 1})
for (int z : new int[]{m.minZ() + 1, m.centerZ(), m.maxZ() - 1}) {
if (!p.owns(x, z)) { p.reason = "outside island"; return null; }
int anchor = m.minY() - 1;
while (anchor >= m.minY() - 17 && !(p.solid(x, anchor, z) && p.solid(x, anchor - 1, z))) anchor--;
if (anchor >= m.minY() - 17) {
boolean compatible = true;
for (int y = anchor - 1; y < m.minY(); y++) if (p.protectedAt(x, y, z)) {
if (!p.selectiveFootings) { p.reason = "protected footing"; return null; }
compatible = false; break;
}
// Do not place this pier, and do not count its quadrant as a bearing.
// The unchanged wall/roof checks and maximum free span still apply.
if (!compatible) continue;
if (anchor < m.minY() - 1) supports.add(new Support(x, anchor + 1, z, m.minY() - 1));
quadrants |= 1 << ((x < m.centerX() ? 0 : 1) + (z < m.centerZ() ? 0 : 2));
}
}
// Existing continuous masonry can also bear on untouched rock beside its walls or roof.
// These contact cells lie in the protected one-block fringe, never inside excavated air.
int wallMask = 0, roofMask = 0;
for (int y : new int[]{m.minY() + 3, m.minY() + 6, m.kind() == Kind.ACCESS ? Math.min(m.maxY() - 5, m.minY() + 18) : m.maxY()}) {
if ((m.openings() & 8) == 0 && p.solid(m.minX() - 1, y, m.centerZ()) && p.solid(m.minX() - 1, y + 1, m.centerZ())) wallMask |= 1;
if ((m.openings() & 2) == 0 && p.solid(m.maxX() + 1, y, m.centerZ()) && p.solid(m.maxX() + 1, y + 1, m.centerZ())) wallMask |= 2;
if ((m.openings() & 1) == 0 && p.solid(m.centerX(), y, m.minZ() - 1) && p.solid(m.centerX(), y + 1, m.minZ() - 1)) wallMask |= 4;
if ((m.openings() & 4) == 0 && p.solid(m.centerX(), y, m.maxZ() + 1) && p.solid(m.centerX(), y + 1, m.maxZ() + 1)) wallMask |= 8;
}
if (m.kind() != Kind.ACCESS) for (int x : new int[]{m.minX() + 1, m.maxX() - 1})
for (int z : new int[]{m.minZ() + 1, m.maxZ() - 1}) if (p.solid(x, m.maxY() + 1, z) && p.solid(x, m.maxY() + 2, z))
roofMask |= 1 << ((x < m.centerX() ? 0 : 1) + (z < m.centerZ() ? 0 : 2));
boolean anchored = quadrants == 15 || roofMask == 15 || (wallMask & 3) == 3 || (wallMask & 12) == 12;
if (!anchored && m.kind() != Kind.GALLERY) { p.reason = "no civil-engineering bearing for " + m.kind(); return null; }
int step = full ? 1 : 4;
for (int x = m.minX(); x <= m.maxX(); x += step) for (int z = m.minZ(); z <= m.maxZ(); z += step) {
if (!p.owns(x, z)) { p.reason = "outside island"; return null; }
for (int y = m.minY(); y <= m.maxY(); y += step) if (p.protectedAt(x, y, z)) {
p.reason = "protected " + m.kind() + " at " + x + "," + y + "," + z; return null;
}
}
var result = new Module(m.kind(), m.minX(), m.minY(), m.minZ(), m.width(), m.height(), m.depth(),
m.facing(), m.openings(), m.seed(), supports);
p.anchored.put(result, anchored); return result;
}
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
private static final class Probe {
final Terrain terrain; final Set<Long> columns = new HashSet<>();
final Map<Long, Integer> entrances = new HashMap<>(); final Map<Module, Boolean> anchored = new HashMap<>();
int samples; boolean selectiveFootings; String reason = "budget";
Probe(Terrain terrain) { this.terrain = terrain; }
void visit(int x, int z) {
if (samples >= TransitPlan21.MAX_SAMPLES) throw new Budget(); samples++;
long key = key(x, z);
if (!columns.contains(key) && columns.size() >= MAX_COLUMNS) throw new Budget(); columns.add(key);
}
boolean owns(int x, int z) { visit(x, z); return terrain.owns(x, z); }
boolean solid(int x, int y, int z) { visit(x, z); return terrain.solid(x, y, z); }
boolean protectedAt(int x, int y, int z) { visit(x, z); return terrain.protectedAt(x, y, z); }
int surface(int x, int z) { visit(x, z); return terrain.surfaceY(x, z); }
}
private static final class Budget extends RuntimeException { private static final long serialVersionUID = 1L; }
}
@@ -26,7 +26,8 @@ public final class TransitStructure21 extends Structure {
}
return Optional.of(new GenerationStub(TransitPiece21.locator(modules.getFirst()), pieces ->
modules.forEach(module -> pieces.addPiece((generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
&& module.kind() == TransitPlan21.Kind.JUNCTION && Integer.bitCount(module.openings()) == 2
&& module.kind() == TransitPlan21.Kind.JUNCTION
&& (generator.rootGeneration() == 24 || Integer.bitCount(module.openings()) == 2)
? new TransitPiece22(module) : new TransitPiece21(module)))));
}
@Override public StructureType<?> type() { return TransitStructures21.TYPE; }
@@ -58,7 +58,9 @@ public final class TransitStructures21 {
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
var start = holder.value().generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
chunk, holder.value().biomes()::contains);
// The route is already certified as one connected whole. A 3D biome boundary
// must not silently remove a local segment or one of its two surface accesses.
chunk, generator.rootGeneration() == 24 ? biome -> true : holder.value().biomes()::contains);
if (start.isValid()) manager.setStartForStructure(holder.value(), start, chunk);
}
/** Locate selects a saved surface portico rather than teleporting the player into a deep road. */
@@ -50,6 +50,7 @@
"sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath",
"sanctuary:woodland_sulfur_depths"
"sanctuary:woodland_sulfur_depths",
"sanctuary:swamp27_caves"
]
}
@@ -63,5 +63,9 @@
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
},
"terraces_29": false,
"relief_30": true,
"surface_302": false,
"balanced_303": true
}
@@ -63,5 +63,9 @@
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
},
"terraces_29": false,
"relief_30": true,
"surface_302": false,
"balanced_303": true
}
@@ -63,5 +63,9 @@
"noise": "minecraft:erosion",
"xz_scale": 2.0,
"y_scale": 2.0
}
},
"terraces_29": false,
"relief_30": true,
"surface_302": false,
"balanced_303": true
}
@@ -0,0 +1,83 @@
package fr.koka.sanctuary.expansion;
import java.util.List;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces;
import net.minecraft.world.level.levelgen.structure.structures.MineshaftStructure;
/** Thin native branches must not reserve every empty room between them. Native support reach stays bounded. */
public final class MineshaftAdmission29Smoke {
public static void main(String[] args) {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
// Bootstrap does not load datapack tags. This synthetic world contains only STONE/AIR;
// bind its real26.3 heightmap tag so the production native predicate can see its stone ceiling.
var blocks = net.minecraft.core.registries.BuiltInRegistries.BLOCK;
blocks.freeze();
blocks.prepareTagReload(new net.minecraft.tags.TagLoader.LoadResult<>(net.minecraft.core.registries.Registries.BLOCK,
java.util.Map.of(net.minecraft.tags.BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP,
List.of(blocks.wrapAsHolder(Blocks.STONE))))).apply();
require(net.minecraft.world.level.levelgen.Heightmap.Types.OCEAN_FLOOR_WG.isOpaque().test(Blocks.STONE.defaultBlockState()),
"The synthetic stone ceiling is bound to the native heightmap tag");
var branches = List.of(MineshaftAdmission29.body(new BoundingBox(-30,150,-30,-28,152,30)),
MineshaftAdmission29.body(new BoundingBox(28,150,-30,30,152,30)),
MineshaftAdmission29.body(new BoundingBox(-30,150,28,30,152,30)));
var centralSecret = new BoundingBox(-5,145,-5,5,160,5);
require(new BoundingBox(-30,149,-30,30,154,30).intersects(centralSecret), "Old global envelope falsely intersects the central secret");
require(!MineshaftAdmission29.intersects(branches, centralSecret), "Unoccupied room between native branches is not part of the mine");
require(MineshaftAdmission29.intersects(branches, new BoundingBox(-31,151,0,-29,152,1)), "An actual branch remains protected against an intersecting secret");
var layered = column(-1,384);
for (int y = 180; y <= 220; y++) layered.setBlock(y, Blocks.STONE.defaultBlockState());
var layers = MineshaftHeights29.choose(List.of(new MineshaftHeights29.Probe(layered,-6,6,false)),54,16,340);
require(layers.size()==3 && layers.stream().allMatch(y->y>=186 && y<=214), "A low local surface estimate cannot send all mine trials below the real rock mass");
require(Math.abs(layers.get(0)-layers.get(1))>=8 && Math.abs(layers.get(1)-layers.get(2))>=8, "The three native heights remain distinct");
require(MineshaftHeights29.choose(List.of(new MineshaftHeights29.Probe(column(-1,384),-6,6,false)),54,16,340).isEmpty(), "Empty columns never manufacture an underground candidate");
var crossing = new MineshaftPieces.MineShaftCrossing(0,new BoundingBox(0,150,0,4,152,4),Direction.SOUTH,MineshaftStructure.Type.NORMAL);
var approach = new MineshaftPieces.MineShaftCorridor(0,RandomSource.create(42),new BoundingBox(1,150,-15,3,152,-1),Direction.NORTH,MineshaftStructure.Type.NORMAL);
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->z<0?column(140,384):column(-1,240),384)==null,
"An interior native plank crossing can join a carried corridor below a distant cavern ceiling");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->column(149,384),384)==null,
"A sturdy crossing floor needs no rock roof");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing),(x,z)->column(-1,240),384)!=null,
"An isolated crossing cannot borrow a nonexistent carried corridor");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing),(x,z)->column(149,384),384)==null,
"A wholly natural crossing floor carries itself without an invented extra corridor");
require(MineshaftCrossing29.rejection(crossing,List.of(crossing,approach),(x,z)->z<0?column(140,384):column(-1,384),384)!=null,
"An open-sky crossing over void has no floor that vanilla would place");
for (var direction : List.of(Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST)) {
boolean z = direction.getAxis() == Direction.Axis.Z;
var piece = new MineshaftPieces.MineShaftCorridor(0, RandomSource.create(42),
new BoundingBox(0,150,0,z?2:14,152,z?14:2),direction,MineshaftStructure.Type.NORMAL);
var below = MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(140,180));
require(below!=null && below.size()==4 && below.stream().allMatch(b->b.minY()==140 && b.maxY()==149), "Rotated wooden feet reach real rock below");
var above = MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(-1,160));
require(above!=null && above.stream().allMatch(b->b.minY()==149 && b.maxY()==160), "Suspension-chain bounds include the real rock anchor");
var ledge = column(-1,384); ledge.setBlock(149, Blocks.STONE.defaultBlockState());
var direct = MineshaftSupport28.anchorVolumes(piece,(x,zz)->ledge);
require(direct!=null && direct.size()==4 && direct.stream().allMatch(b->b.minY()==149 && b.maxY()==149), "A sturdy natural ledge is already the native floor in every rotation");
require(!MineshaftSupport28.anchored(piece,(x,zz)->ledge), "The legacy support-only path remains unchanged");
var oneSide = MineshaftSupport28.anchorVolumes(piece,(x,zz)->
(direction.getAxis()==Direction.Axis.Z ? x==0 : zz==0) ? column(140,384) : column(-1,384));
require(oneSide!=null && oneSide.size()==2, "Native optional opposite pillars may be absent when both transverse ends remain anchored");
var firstEnd = SwampHutPlacement28.worldPosition(piece,0,-1,2);
require(MineshaftSupport28.anchorVolumes(piece,(x,zz)->
(direction.getAxis()==Direction.Axis.Z ? zz==firstEnd.getZ() : x==firstEnd.getX()) ? column(140,384) : column(-1,384))==null,
"A corridor with an entirely unattached end is still refused");
require(MineshaftSupport28.anchorVolumes(piece,(x,zz)->column(-1,220))==null, "A completely unsupported corridor is still refused");
require(MineshaftAdmission29.intersects(above,new BoundingBox(above.getFirst().minX(),158,above.getFirst().minZ(),above.getFirst().maxX(),159,above.getFirst().maxZ())), "A secret along the chain outside the corridor body is detected");
}
System.out.println("MineshaftAdmission29Smoke: native branch volumes, central gaps, four orientations and bounded floor/chain anchors passed.");
}
private static NoiseColumn column(int floor,int ceiling) {
var states=new BlockState[384];
for(int y=0;y<states.length;y++) states[y]=(y<=floor||y>=ceiling?Blocks.STONE:Blocks.AIR).defaultBlockState();
return new NoiseColumn(0,states);
}
private static void require(boolean condition,String message) {if(!condition)throw new AssertionError(message);}
}
@@ -0,0 +1,38 @@
package fr.koka.sanctuary.worldgen;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class BalancedRelief303Smoke {
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
public static void main(String[] args){
DensitySampler noise=new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return (float)(Math.sin(x*.05)+Math.cos(z*.04));}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
for(int radius:new int[]{256,362,512}){
var p=new BalancedRelief303(noise,radius,0);int flat=0,mountain=0,n=0;
for(int z=-radius;z<=radius;z+=8)for(int x=-radius;x<=radius;x+=8){
if(Math.hypot(x,z)>radius*.8)continue;
double w=p.province(x,z);if(w==0)flat++;if(w>.5)mountain++;n++;
if(Math.hypot(x,z)<=48)require(w==0,"Natural arrival remains untouched");
}
require(flat>n*.4 && mountain>n*.05,"Every size combines broad natural plateaus and mountains");
}
for(double w=0;w<=1;w+=.05){
double previous=Double.NEGATIVE_INFINITY;
for(int y=0;y<=384;y++){
double now=BalancedRelief303.sourceY(y,w);
require(now>previous,"Vertical mapping cannot fold rock into repeated layers");previous=now;
if(y<=170||w==0)require(now==y,"Zero mountain influence and lower caves preserve original coordinates");
}
}
require(BalancedRelief303.ceilingErosion(358)>=1,"Even fully dense rock fades before Y360");
// New fragment cleanup also covers raised summits, outside the former302 height range.
DensitySampler singleton=new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return x==96&&y==355&&z==100?1:-1;}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
};
require(new DetachedRock302(singleton,362,0,true).sampleValue(SamplerContext.EMPTY_UNCACHED,96,355,100)<0,"A detached summit block is removed");
System.out.println("BalancedRelief303Smoke: scale balance, untouched arrival/depths, monotonic domains, ceiling and detached summit passed");
}
}
@@ -0,0 +1,39 @@
package fr.koka.sanctuary.worldgen;
import java.util.*;
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
public final class River30Smoke {
public static void main(String[] args) {
var cells = new LinkedHashMap<Long,Cell>();
var path = List.of(new Point(0,0),new Point(-1,2),new Point(-2,3),new Point(-4,4));
cells.put(key(0,0),new Cell(0,0,230,227,233,ShoreMaterial.GRAVEL,77,2));
// Existing dry sediment is precisely what alpha30 incorrectly treated as an obstacle.
for(int x=1;x<=4;x++)for(int z=-2;z<=2;z++)
cells.put(key(x,z),new Cell(x,z,-1,230,234,ShoreMaterial.SAND,77,2));
var original=PopulationHydrology.additional30(cells,
List.of(new Feature(77,Kind.RIVER,230,path,1,new Bounds(0,-2,4,2))),List.of(),0,0);
Sampler coast=(x,y,z)->x<=4 && y<=234 ? 1 : -1;
var opened=BankOutlets30.openBanks(42,original,coast,2);
require(opened.spills().size()==1,"One short cut through an existing dry bank reaches the void");
require(opened.features().stream().filter(f->f.kind()==Kind.RIVER).toList().equals(original.features()),"Original curve and river identity unchanged");
require(opened.cells().stream().filter(Cell::hasWater).toList().equals(original.cells().stream().filter(Cell::hasWater).toList()),"No water source or new channel is added");
var fall=opened.spills().getFirst();
require(fall.outlet().x()==5 && fall.outlet().y()<fall.source().y(),"Five-block breach steps down for vanilla flow to reach its lip");
for(var cell:opened.cells())if(opened.isOutletCell(cell))
require(cell.sedimentDepth()==0&&!cell.hasWater(),"The breach only removes rock");
require(opened.cells().equals(BankOutlets30.openBanks(42,original,coast,2).cells()),"Deterministic openings");
require(BankOutlets30.openBanks(42,original,(x,y,z)->1,2).cells().equals(original.cells()),"Inland bank with no void remains intact");
for(int radius:new int[]{256,362,512}) {
int global=0,total=0,plateau=0;
for(int x=-radius;x<=radius;x+=8)for(int z=-radius;z<=radius;z+=8)if(Math.hypot(x,z)<radius) {
double w=Relief30.influence(42,x,z,radius);total++;
require(w>=0&&w<=1,"Weight is bounded");if(w==1)global++;if(w==0)plateau++;
}
require(global>total*.85&&plateau>0&&plateau<total*.08,"Global noise covers most of the island, with one small plateau area");
}
System.out.println("River30.1: original curves and water retained, dry-bank breach, real downward lip, determinism, inland retention and predominantly global relief passed.");
}
static long key(int x,int z){return ((long)x<<32)^(z&0xffffffffL);}
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
}
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.worldgen;
import java.util.*;
import net.minecraft.world.level.levelgen.densityfunction.*;
public final class SurfaceRelief302Smoke {
private record P(int x,int y,int z) {}
private static DetachedRock302 filter(Set<P> blocks){
return new DetachedRock302(new DensitySampler(){
@Override public float sampleValue(SamplerContext c,int x,int y,int z){return blocks.contains(new P(x,y,z))?.5F:-.5F;}
@Override public void sampleVolume(SamplerContext c,DensityBuffer b,DensityVolume v){DensitySampler.sampleVolumeNaive(c,b,v,this);}
},362,0);
}
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
public static void main(String[] args){
var context=SamplerContext.EMPTY_UNCACHED;var cube=new HashSet<P>();
for(int x=95;x<=96;x++)for(int y=230;y<=231;y++)for(int z=100;z<=101;z++)cube.add(new P(x,y,z));
var removed=filter(Set.copyOf(cube));
for(var p:cube)require(removed.sampleValue(context,p.x,p.y,p.z)<0,"Eight detached blocks crossing a chunk edge must disappear");
for(int x=95;x<=97;x++)for(int y=230;y<=232;y++)for(int z=100;z<=102;z++)cube.add(new P(x,y,z));
var clump=filter(Set.copyOf(cube));
for(var p:cube)require(clump.sampleValue(context,p.x,p.y,p.z)<0,"A detached 3 by 3 by 3 clump is also removed");
for(int z=100;z<=102;z++)for(int x=95;x<=97;x++)cube.add(new P(x,233,z));
var retained=filter(Set.copyOf(cube));
var order=new ArrayList<>(cube);Collections.reverse(order);
for(var p:order)require(retained.sampleValue(context,p.x,p.y,p.z)>0,"A connected body of 36 blocks must remain");
require(retained.sampleValue(context,94,230,100)<0,"Cleanup never fills surrounding air");
var singleton=filter(Set.of(new P(96,230,100)));
require(singleton.sampleValue(context,96,230,100)<0,"A different sampler cannot inherit another body's cached support");
var outside=new P(0,64,0);require(filter(Set.of(outside)).sampleValue(context,0,64,0)>.0,"The arrival and deep field are untouched");
for(double y=-10;y<400;y+=.125)for(float noise:new float[]{-2,-1,-.25F,0,.25F,1,2}){
require(Math.abs(SurfaceRelief302.sourceY(y,1,noise)-y)<=2.25001,"Local ledges must remain bounded");
require(SurfaceRelief302.sourceY(y,0,noise)==y,"An excluded region retains its exact field");
}
System.out.println("SurfaceRelief302Smoke: detached fragments across chunks removed, connected body and void retained, cache isolation and bounded local ledges passed");
}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuary.worldgen;
public final class Terraces29Smoke {
public static void main(String[] args) {
double largest = 0;
int positive = 0, negative = 0, varied = 0;
for (int y = 0; y < 384; y++) {
if (y <= Terraces29.START_Y) require(Terraces29.weight(y, 1) == 0, "lower caves stay unchanged");
require(Terraces29.weight(y, 0) == 0, "arrival and outer coast remain unchanged");
}
for (float coarse : new float[]{-1, -.5F, 0, .5F, 1})
for (float detail : new float[]{-1, -.4F, 0, .6F, 1}) {
double previous = Terraces29.sourceY(0, coarse, detail, 1);
for (int step = 1; step <= 4000; step++) {
double y = step * .1;
double mapped = Terraces29.sourceY(y, coarse, detail, 1);
double offset = mapped - y;
require(Math.abs(offset) <= Terraces29.MAX_OFFSET + 1e-10, "fine terraces stay within 2.5 blocks");
require(mapped > previous && mapped - previous < .5, "fixed local phase remains continuous and monotone");
require(Terraces29.sourceY(y, coarse, detail, 0) == y, "disabled terraces are exactly neutral");
if (offset > .5) positive++;
if (offset < -.5) negative++;
if (Math.abs(mapped - Terraces29.sourceY(y, coarse + .1F, detail - .1F, 1)) > .1) varied++;
largest = Math.max(largest, Math.abs(offset));
previous = mapped;
}
}
require(positive > 0 && negative > 0 && largest > 1.5, "fine relief has both directions at a visible but small amplitude");
require(varied > 0, "3D phase and period do not produce a common horizontal grid");
System.out.println("Terraces29Smoke: bounded local ledges, varied phase, continuous sampling and protected depths passed; max=" + largest);
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,65 @@
package fr.koka.sanctuary.worldgen.transit;
import java.util.*;
/** Pure geometry controls; actual terrain, starts and walkability are checked by Transit29WorldGameTests. */
public final class Transit29Smoke {
public static void main(String[] args) {
var terrain = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return Math.hypot(x, z) < 510; }
public boolean solid(int x, int y, int z) { return y >= 55 && y <= surfaceY(x, z); }
public boolean protectedAt(int x, int y, int z) { return false; }
// This three-wide natural threshold is walkable without flattening a five-wide platform.
public int surfaceY(int x, int z) { return 250 + Math.floorMod(x, 6) / 2; }
};
for (int diameter : new int[]{512, 724, 1024}) for (long seed : new long[]{0, 42, 2026}) {
var plan = TransitPlanner29.plan(seed, 0, 0, diameter, terrain);
require(!plan.isEmpty(), "Supported hillside must admit a traversal: " + plan.diagnostic());
require(plan.equals(TransitPlanner29.plan(seed, 0, 0, diameter, terrain)), "Planning must be deterministic");
var accesses = plan.modules().stream().filter(m -> m.kind() == TransitPlan21.Kind.ACCESS).toList();
int span = Math.max(Math.abs(accesses.getFirst().centerX() - accesses.getLast().centerX()),
Math.abs(accesses.getFirst().centerZ() - accesses.getLast().centerZ()));
require(span >= diameter / 4, "A small local gallery is not the main traversal");
require(plan.modules().stream().anyMatch(m -> m.kind() == TransitPlan21.Kind.HALL), "Keep the established maintenance room where it fits");
var reached = new HashSet<TransitPlan21.Module>(); reached.add(accesses.getFirst());
for (int pass = 0; pass < plan.modules().size(); pass++) for (var a : List.copyOf(reached)) for (var b : plan.modules())
if (connected(a, b)) reached.add(b);
require(reached.size() == plan.modules().size(), "Every module and both accesses must connect through opposing doors");
}
var blocked = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return true; }
public boolean solid(int x, int y, int z) { return y <= 250; }
public boolean protectedAt(int x, int y, int z) { return true; }
public int surfaceY(int x, int z) { return 250; }
};
require(TransitPlanner29.plan(0, 0, 0, 1024, blocked).isEmpty(), "Protected fluids and structures cannot be overwritten");
var protectedBed = new TransitPlanner21.Terrain() {
public boolean owns(int x, int z) { return Math.hypot(x, z) < 510; }
public boolean solid(int x, int y, int z) { return y >= 55 && y <= 250; }
public boolean protectedAt(int x, int y, int z) { return y == 222; }
public int surfaceY(int x, int z) { return 250; }
};
var rescued = TransitPlanner29.plan(2026, 0, 0, 512, protectedBed);
require(!rescued.isEmpty() && rescued.diagnostic().contains("selective-footings"),
"After the complete primary search fails, valid walls may carry a route above a protected bed");
for (var m : rescued.modules()) {
require(m.minY() > 222, "The rescued route must not excavate the protected layer");
for (var support : m.supports()) require(support.minY() > 222 || support.maxY() < 222,
"No retained footing may write through the protected layer");
}
System.out.println("Transit29 pure geometry: nine seed/size cases, narrow natural thresholds, connected halls, bounded protected refusal.");
}
private static boolean connected(TransitPlan21.Module a, TransitPlan21.Module b) {
if (a.floorY() != b.floorY()) return false;
if (a.centerX() == b.centerX()) {
if (a.maxZ() + 1 == b.minZ()) return (a.openings() & 4) != 0 && (b.openings() & 1) != 0;
if (b.maxZ() + 1 == a.minZ()) return (b.openings() & 4) != 0 && (a.openings() & 1) != 0;
}
if (a.centerZ() == b.centerZ()) {
if (a.maxX() + 1 == b.minX()) return (a.openings() & 2) != 0 && (b.openings() & 8) != 0;
if (b.maxX() + 1 == a.minX()) return (b.openings() & 2) != 0 && (a.openings() & 8) != 0;
}
return false;
}
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.28"
version = "0.1.0-alpha.30.3"
pack-format = "packwiz:1.1.0"
[index]