Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf327b75d4 | ||
|
|
8c90c61c53 | ||
|
|
9f82577927 | ||
|
|
79d3185cba | ||
|
|
ad234a808f |
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## 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 l’eau.
|
||||
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 l’admission 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 ;
|
||||
bateaux rapprochés et îlots au-dessus de Sanctuary. Marais réduits. Mines,
|
||||
huttes et donjons vanilla adaptés aux souterrains ; filons natifs selon la
|
||||
profondeur dans la roche, dans l’île et les expansions. Nouveau monde de test.
|
||||
Voir [génération](docs/generation-alpha28.md) et [validation](docs/testing-alpha28.md).
|
||||
|
||||
## 0.1.0-alpha.27 — 2026-09-12
|
||||
|
||||
Retour à la base alpha.24 après rejet visuel de l’alpha.25. Le relief et les
|
||||
|
||||
@@ -21,6 +21,32 @@ 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 — 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
|
||||
l’eau 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).
|
||||
L’alpha.29 reste un point de reprise local non publié, intégré à cette livraison.
|
||||
|
||||
## Alpha.28 — grottes et lieux proches
|
||||
|
||||
Cette passe prolonge la 27 validée en jeu : bruits de grottes vanilla dans
|
||||
la roche, failles moins traversantes, reliefs déplacés et affaissés par du bruit 3D,
|
||||
bateaux près des berges et deux îlots au-dessus de Sanctuary. Les marais
|
||||
occupent moins de place. Mines, huttes et donjons vanilla bénéficient de
|
||||
contrôles adaptés aux cavités ; les filons suivent la profondeur de la masse
|
||||
rocheuse, y compris dans les expansions.
|
||||
|
||||
Créer un **nouveau monde**. Voir le [périmètre](docs/generation-alpha28.md),
|
||||
les [résultats effectivement vérifiés](docs/testing-alpha28.md) et la
|
||||
[distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.27 — retour au terrain de la 24
|
||||
|
||||
La base est le commit `e7d1f29`, sans les massifs coniques, chambres creusées
|
||||
@@ -572,7 +598,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.23 |
|
||||
| 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
|
||||
@@ -601,7 +627,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.23.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).
|
||||
|
||||
@@ -966,3 +966,37 @@ marais volumétriques dans les cavités existantes et affiner les coques. Voir l
|
||||
[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes
|
||||
et les petites sorties d’eau seront traités un à un, avec retour en jeu entre
|
||||
les changements ; ils ne sont pas déclarés implémentés dans cette passe.
|
||||
|
||||
|
||||
### WG-30 — Spéléologie, reliefs et satellites proches — alpha.28
|
||||
|
||||
Branche `codex/caves-relief-alpha28`, depuis la 27 validée visuellement par le
|
||||
créateur. Ajouter les bruits de grottes vanilla dans les masses rocheuses,
|
||||
limiter et incliner les failles, développer des hauteurs avec ridge/erosion,
|
||||
rapprocher les bateaux des vraies berges et placer les îlots au-dessus du terrain.
|
||||
Moins de marais ; mines, huttes et donjons vanilla adaptés aux cavités réelles.
|
||||
Les filons natifs doivent aussi alimenter les continents ouverts ensuite.
|
||||
Aucun retour des salles ellipsoïdales ni des cônes de l’alpha.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 l’alpha.28 validée en jeu
|
||||
sur Windows par le créateur. Reproduire la traversée manquante sur Grand,
|
||||
graine `-7228211907433324401`, corriger l’admission 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 l’eau 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).
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Alpha.28 — roche à explorer
|
||||
|
||||
La 27 est la base validée par le créateur. Cette passe garde son île, les
|
||||
secrets minéraux et les réseaux, puis ajoute du creusement dans la roche.
|
||||
Les nouvelles cavités ne sont pas des salles de structure aux volumes prescrits.
|
||||
|
||||
## Terrain
|
||||
|
||||
Le champ commun compose les fonctions de bruit de Minecraft 26.3-pre-2 :
|
||||
cheese et cave_layer, entrées/spaghetti 3D, spaghetti 2D et roughness, piliers
|
||||
et noodle. Leur altitude est déplacée pour la masse flottante. Les coupes
|
||||
restent soustractives et s’atténuent vers les limites verticales ; il ne
|
||||
s’agit pas de recopier toute la génération Overworld ou ses aquifères.
|
||||
Les carvers historiques restent désactivés pour ne pas ajouter une seconde
|
||||
passe étrangère aux colonnes de certification.
|
||||
|
||||
Le relief supérieur déforme le volume rocheux dans les trois axes avec les
|
||||
bruits ridge/erosion natifs échantillonnés en 3D. Les déplacements sont signés :
|
||||
soulèvements, affaissements et cisaillements varient aussi avec la profondeur.
|
||||
Le déplacement est borné à ±16 blocs horizontalement et ±40 verticalement,
|
||||
avec une transition progressive de Y180 à Y228. L’enveloppe de l’île reste
|
||||
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
|
||||
Ce n’est pas une simulation physique de plaques tectoniques. Aucun cône ni
|
||||
salle géométrique ne sert de forme à ajouter au terrain.
|
||||
L’arrivée et les côtes gardent leur place. Les anciennes failles deviennent
|
||||
obliques, interrompues et limitées en hauteur, avec de la roche au-dessus et
|
||||
en dessous. Les expansions utilisent la même source de terrain ; leur climat
|
||||
et leur déformation propre s’appliquent ensuite.
|
||||
|
||||
Le seuil du champ de marais passe de 0,04 à 0,25 : les anciens secteurs se
|
||||
réduisent au profit des biomes de grottes précédents, dont les champignons.
|
||||
La hauteur du monde, les nuages et les trois diamètres sont conservés.
|
||||
|
||||
## Exploration et ressources
|
||||
|
||||
Les bateaux vanilla restaurés recherchent désormais les vraies berges proches.
|
||||
Les deux îlots sont au-dessus de Sanctuary, avec un espace libre sous leur
|
||||
roche et un seul exutoire d’eau. Leurs corps et leurs chutes d’eau ont des
|
||||
emprises distinctes ; rien ne réserve toute la colonne sous un îlot sec.
|
||||
|
||||
Les mines emploient les pièces vanilla, leurs supports, rails, coffres et
|
||||
spawners. Une partie exposée peut passer si l’ensemble reste ancré. Les huttes
|
||||
cherchent aussi des berges souterraines de marais : sol local, couverture de
|
||||
la cavité et quatre pilotis natifs. Les salles à spawner sont de vraies
|
||||
MonsterRoomFeature, posées sur des candidats réels et soumises aux refus
|
||||
natifs. Leur présence n’est pas garantie sur chaque île.
|
||||
|
||||
Une passe de filons native complète les distributions existantes dans les
|
||||
chunks de l’île et des expansions. Charbon, fer, cuivre, or, lapis, redstone,
|
||||
diamant et émeraude utilisent les formes OreFeature vanilla. La profondeur
|
||||
se mesure dans l’enveloppe rocheuse entière de la colonne, sans recommencer
|
||||
le calcul à chaque plafond de grotte. Les stocks restent finis. Les minerais
|
||||
du Nether restent dans le Nether ; le Deep Dark n’est pas ajouté à l’île.
|
||||
|
||||
## Livraison de test
|
||||
|
||||
Mod et pack `0.1.0-alpha.28`, Minecraft `26.3-pre-2`, codec de prototype
|
||||
`sanctuary:island_v24` conservé. Créer un nouveau monde. Aucune migration,
|
||||
aucune régénération ni intervention dans les sauvegardes personnelles.
|
||||
L’alpha.27 et ses artefacts restent disponibles. Les vérifications réelles
|
||||
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
|
||||
@@ -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 n’est 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 l’arrivé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 s’adapter 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 d’altitude 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 n’est 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 n’impose 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 d’une sauvegarde personnelle. Les artefacts28 restent accessibles.
|
||||
Les constats mesurés et les limites sont consignés dans [les essais](testing-alpha29.md).
|
||||
@@ -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 n’a 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 s’agit de déplacer
|
||||
et d’affaisser la roche existante, pas d’ajouter 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
|
||||
l’argile. Les petits bassins et réseaux précédents restent présents.
|
||||
|
||||
Quand une berge est séparée d’un 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 n’impose pas deux cascades terminales, ni de bassin de
|
||||
réception : l’eau peut descendre sur l’île ou dans le vide. La brèche ne pose
|
||||
ni fondation ni colonne d’eau artificielle ; les ticks vanilla propagent l’eau
|
||||
depuis les sources voisines. Les emprises de chute restent réservées aux
|
||||
expansions. L’arrivé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 l’eau. Ce n’est
|
||||
pas une simulation de bassin versant avec débit conservé ou pente monotone.
|
||||
Ces ajouts concernent l’île initiale ; les profils des continents d’expansion
|
||||
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 d’exposition, 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 lorsqu’ils 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 l’hydrologie va creuser en bassin
|
||||
ou en brèche ; aucun bloc artificiel n’est 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 n’y a ni migration ni régénération d’une 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).
|
||||
+46
-4
@@ -13,7 +13,37 @@ 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.27 prête à publier — 12 septembre 2026
|
||||
## 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)
|
||||
est publiée depuis `79d3185`. Elle prolonge la 27 avec les cavités natives,
|
||||
la déformation 3D signée du relief, des failles bornées, les mines/donjons et
|
||||
les filons, moins de marais, deux bateaux près des berges et deux îlots au-dessus
|
||||
de Sanctuary. Les huttes restent conditionnelles. Voir
|
||||
[la génération](generation-alpha28.md) et [les résultats](testing-alpha28.md).
|
||||
|
||||
`check build` passe sur Mac : **21/21 GameTests natifs**. `assemblePack`,
|
||||
JAR, MRpack et ZIP d’amorçage sont vérifiés localement puis par téléchargement
|
||||
public. Minecraft 26.3-pre-2 et les dépendances restent inchangés.
|
||||
|
||||
Le [MRpack autonome](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.28/Sanctuary-0.1.0-alpha.28.mrpack)
|
||||
est disponible. Le canal packwiz est au commit
|
||||
`6ec1c10c710870a2ccc591b524cc26d125ca03a2`. Deux synchronisations isolées,
|
||||
puis deux dans l’instance existante **Sanctuary Beta**, réussissent.
|
||||
Un seul JAR Sanctuary alpha.28 est actif ; les **925 fichiers personnels
|
||||
et réglages suivis** gardent leurs empreintes. Le jeu était fermé.
|
||||
Sauvegarde ciblée : `sanctuary-backups/before-0.1.0-alpha.28/`.
|
||||
|
||||
SHA-256 JAR : `7f95e9ebd50063c7ebfc649fa325e75263649e42c02e7159244953e09e27d24e`.
|
||||
SHA-256 MRpack : `12d4db8f06a13fb19feb08e61325361f7ab4d783d6f43b3d0c3582d316c54d0f`.
|
||||
Reçus : `build/alpha28-delivery-public.json`,
|
||||
`build/alpha28-isolated-validation.json`, `build/alpha28-prism-validation.json`.
|
||||
|
||||
Créer un **nouveau monde Sanctuary** ; témoin **Moyen 724, graine 0**.
|
||||
Les tests natifs concernent Mac, pas Windows. Le rendu client et les surplombs
|
||||
restent à juger en jeu. Aucun monde personnel n’a été régénéré.
|
||||
|
||||
## Alpha.27 publiée — 12 septembre 2026
|
||||
|
||||
La reprise repart directement de l’alpha.24 : relief et cavités conservés,
|
||||
marais souterrains localisés, montgolfière retirée et navires fondés sur le
|
||||
@@ -22,9 +52,21 @@ Voir [le périmètre](generation-alpha27.md) et [les essais](testing-alpha27.md)
|
||||
Minecraft 26.3-pre-2 et les dépendances de la 24 sont conservés.
|
||||
|
||||
`check build` puis `assemblePack` passent sur Mac. Le JAR, le MRpack et le ZIP
|
||||
d’amorçage sont vérifiés localement. La publication et la synchronisation de
|
||||
l’instance existante sont les prochaines étapes ; elles ne sont pas encore
|
||||
attestées par ce commit.
|
||||
d’amorçage sont vérifiés localement puis par téléchargement public.
|
||||
|
||||
La [release alpha.27](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.27)
|
||||
est publiée depuis `392ade0`. Le canal packwiz est au commit
|
||||
`9803b2279aa5242c9e3e7202b940bf47e1c3f404`. Le
|
||||
[MRpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.27/Sanctuary-0.1.0-alpha.27.mrpack)
|
||||
est disponible séparément.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans la même instance
|
||||
Prism **Sanctuary Beta** réussissent. Un seul JAR Sanctuary alpha.27 est actif.
|
||||
Les **925 fichiers personnels et réglages suivis** gardent leurs empreintes ;
|
||||
le jeu était fermé. Le dossier reste `Sanctuary-0.1.0-alpha.1`, avec sauvegarde
|
||||
ciblée dans `sanctuary-backups/before-0.1.0-alpha.27/`.
|
||||
Reçus locaux : `build/alpha27-delivery-public.json`,
|
||||
`build/alpha27-isolated-validation.json`, `build/alpha27-prism-validation.json`.
|
||||
|
||||
SHA-256 JAR : `56144b291407650e148aa3769596165a5e87398ea4a847cf67352ce2a2c625bf`.
|
||||
SHA-256 MRpack : `e13be90c6454bad3cdd3418fc54b827488700876f6e4ddb42e4a05f57acfecb6`.
|
||||
|
||||
@@ -95,3 +95,11 @@ Aucun essai Windows natif, mesure de FPS ou validation visuelle par le joueur
|
||||
n'est revendiqué. La durée du build comprend les vérifications et ne mesure
|
||||
pas le temps de chargement d'une partie. La publication des artefacts et la
|
||||
synchronisation Prism font l'objet de reçus de livraison distincts.
|
||||
|
||||
## Livraison
|
||||
|
||||
`assemblePack` réussit. JAR, MRpack, dépendances et ZIP d’amorçage vérifiés,
|
||||
puis téléchargement anonyme des mêmes artefacts. Deux installations isolées
|
||||
et deux synchronisations de l’instance Prism existante réussissent ; les
|
||||
925 fichiers personnels et réglages suivis sont inchangés. Voir
|
||||
[la livraison](packwiz.md#alpha27-publiée--12-septembre-2026).
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Alpha.28 — vérifications
|
||||
|
||||
## Protocole
|
||||
|
||||
Tests sur macOS avec Java 25 et Minecraft 26.3-pre-2. Aucun essai Windows
|
||||
natif ni jugement de rendu client n’est revendiqué. Le créateur a validé
|
||||
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
|
||||
nouvelle passe, sans représenter l’eau, la végétation ou les structures.
|
||||
|
||||
Commande de livraison :
|
||||
|
||||
```sh
|
||||
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerial24Tests=true
|
||||
```
|
||||
|
||||
`sanctuaryDensityMaps=false` coupe la production des anciennes images,
|
||||
pas les assertions. Le monde GameTest est recréé dans le dossier de développement
|
||||
ignoré. Les sauvegardes personnelles ne sont pas utilisées par les tests.
|
||||
|
||||
Les smokes de placement aérien examinent 512 / 724 / 1 024, graines 0 / 42 /
|
||||
2026. Le témoin natif chargé en chunks FULL est Moyen 724, graine 0 ; ceci
|
||||
ne prétend pas être neuf îles entièrement explorées. Les comparaisons de
|
||||
terrain analysent deux coupes espacées de 2 blocs et une grille de hauteurs
|
||||
espacée de 32 blocs. Une expansion naturelle de 512 est contrôlée par son
|
||||
vrai champ de densité sans être réservée ni ouverte dans le serveur.
|
||||
|
||||
## Résultats
|
||||
|
||||
`check build` passe en **4 min 15 s**, dont **21/21 GameTests natifs**
|
||||
en 57,06 s. `assemblePack` passe ensuite sans recompilation des binaires vérifiés.
|
||||
Les smokes couvrent aussi le Blocodex, les formats de journal, les priorités
|
||||
et les réservations d’expansion, ainsi que les anciennes géométries pures.
|
||||
|
||||
Le terrain de référence conserve **6 098 des 6 860** anciens échantillons
|
||||
rocheux intérieurs (88,9 %), avec **762** désormais creusés. Cette comparaison
|
||||
inclut les raccords du nouveau cisaillement, pas seulement les grottes.
|
||||
L’expansion de 512 conserve 1 299 échantillons contre 136 creusés.
|
||||
|
||||
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 s’abaissent.
|
||||
La hausse maximale relevée est de 32 blocs et le sommet échantillonné atteint
|
||||
Y280. La baisse maximale de sommet, 132 blocs en (0,160), correspond à un
|
||||
lobe supérieur qui quitte cette colonne : le sol observé passe de Y248 à
|
||||
Y116. Elle ne désigne pas un déplacement vertical de 132 blocs ; le déplacement
|
||||
3D est borné à ±40 en Y. Des surplombs et étranglements subsistent et restent
|
||||
à apprécier visuellement dans le jeu.
|
||||
|
||||
Les **9 chunks FULL** inspectés contiennent les huit minerais Overworld :
|
||||
|
||||
| Minerai | Blocs observés, variantes deepslate comprises |
|
||||
|---|---:|
|
||||
| Charbon | 1408 |
|
||||
| Fer | 325 |
|
||||
| Cuivre | 302 |
|
||||
| Or | 87 |
|
||||
| Lapis | 53 |
|
||||
| Redstone | 77 |
|
||||
| Diamant | 16 |
|
||||
| Émeraude | 4 |
|
||||
|
||||
Il s’agit d’un relevé après génération, pas de quotas imposés à chaque chunk.
|
||||
|
||||
La relecture finale des fichiers de région confirme **trois donjons vanilla**
|
||||
avec spawners zombies et coffres `minecraft:chests/simple_dungeon` :
|
||||
`-101 170 100`, `-92 175 -92` et `-85 178 11`. Une vraie mine est présente
|
||||
vers `152 176 -120`, avec **143 pièces sauvegardées**. Dans les 14 chunks FULL
|
||||
du contrôle des structures : 91 planches, 88 barrières, 13 rails, 29 toiles
|
||||
et un spawner ont été observés dans les pièces chargées. Ce relevé sur les
|
||||
fichiers MCA après fermeture du serveur ne génère aucun chunk supplémentaire.
|
||||
|
||||
Les quatre lieux aériens ont de vrais départs et pièces sauvegardées,
|
||||
quatre coffres et deux marchands adultes au total, puis passent les contrôles
|
||||
après ouverture, extraction et modification dans le protocole natif. Il y a
|
||||
exactement une source de cascade et deux îlots survolant le terrain. Aucun
|
||||
nouveau site ni habitant n’est recréé pour remplacer ce qui a été utilisé.
|
||||
|
||||
Preuves locales ignorées : `build/alpha28-check-build-final.log`,
|
||||
`build/alpha28-preview/geology-final.json`,
|
||||
`build/alpha28-preview/geology-final-comparison.png`,
|
||||
`build/alpha28-evidence/final/` et reçus de distribution `build/alpha28-*`.
|
||||
|
||||
|
||||
## Limites et essai du créateur
|
||||
|
||||
- La hutte de sorcière est conditionnelle aux berges 3D, à l’espace et aux quatre
|
||||
appuis natifs. Le premier petit échantillon n’en contient aucune : son audit
|
||||
de placement ne constitue pas une preuve de présence en jeu.
|
||||
- Les quatre départs et les deux UUID de marchands persistants sont relus
|
||||
dans les données sauvegardées. Aucun second lancement JVM avec ce monde
|
||||
alpha.28 n’est revendiqué.
|
||||
- La source de la cascade est contrôlée ; son écoulement complet jusqu’au vide
|
||||
n’est pas mesuré par ce protocole. La propagation reste celle de Minecraft.
|
||||
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
|
||||
l’ancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
|
||||
que le nouveau relief serait identique à celui de la 27.
|
||||
- Il ne s’agit pas d’un benchmark client ni d’une simulation physique tectonique.
|
||||
Les performances pourront être étudiées après ce retour visuel.
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, taille **Moyen**, graine **0** pour
|
||||
retrouver le témoin. Garder ses autres sauvegardes ; aucune migration ni
|
||||
régénération n’est réalisée. Valider en priorité les masses soulevées et
|
||||
abaissées, les cavités à explorer, les îlots au-dessus du terrain et les
|
||||
navires proches des berges.
|
||||
|
||||
## Distribution
|
||||
|
||||
Le JAR, le MRpack et le ZIP d’amorçage ont les mêmes empreintes sur disque
|
||||
et après téléchargement anonyme de la release. Les deux synchronisations
|
||||
isolées sont identiques ; les deux synchronisations de l’instance Prism
|
||||
existante conservent les empreintes des 925 fichiers personnels et réglages
|
||||
suivis. Un seul JAR alpha.28 est actif. Voir [le reçu de livraison](packwiz.md).
|
||||
@@ -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 l’alpha.28 ; aucun essai Windows de la 29 n’est
|
||||
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 d’entré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 n’ouvrent 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 l’admission 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 d’une 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 d’un bloc dans la bande Y205–305, 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 lorsqu’une
|
||||
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. L’inspection 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 n’est 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 d’expansion 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 n’est 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 n’est pas un benchmark de génération.
|
||||
|
||||
`./gradlew assemblePack` passe également. JAR, MRpack autonome, index packwiz
|
||||
et ZIP d’amorç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 d’exécution avec structures désactivées propre à l’alpha.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 : l’utiliser en
|
||||
spectateur pour l’inspection, sans supposer une arrivée sûre en survie.
|
||||
@@ -0,0 +1,145 @@
|
||||
# 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 n’est 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 d’environ **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 l’eau 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é n’est 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 n’est
|
||||
pas un inventaire exhaustif de l’île et aucune présence de mine n’y 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 n’est revendiqué sur Petit. La rivière couvre une
|
||||
distance d’environ 362 blocs, avec 2 551 cellules et quatre brèches. Sept chunks
|
||||
FULL sont échantillonnés ; l’eau 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 l’espace 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 l’espace
|
||||
libre au-dessus ; les satellites passent également.
|
||||
|
||||
Le test exigeant une mine sur cette graine échoue : aucun départ de mine n’est
|
||||
retenu parmi ses 19 candidats, y compris via la recherche native. Les **23 autres
|
||||
tests passent**. Le témoin de mine de la 29 n’est 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 l’exigence 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 l’inté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
|
||||
n’a 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 n’est 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 d’un 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 l’absence 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 n’est 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 n’est ni un temps de rendu client, ni un benchmark
|
||||
comparatif avec la 28. Aucun objectif chiffré de performance n’est 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 n’a d’attache 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 d’expansion 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
|
||||
d’amorç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 n’ont pas
|
||||
été modifiés. La publication et la synchronisation de l’instance Mac sont
|
||||
suivies dans [la distribution](packwiz.md).
|
||||
+2
-2
@@ -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.27
|
||||
pack_version=0.1.0-alpha.27
|
||||
mod_version=0.1.0-alpha.30
|
||||
pack_version=0.1.0-alpha.30
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -56,6 +56,10 @@ 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.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'))
|
||||
@@ -623,3 +627,41 @@ tasks.register('swamp27VolumesSmoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.Swamp27VolumesSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('swamp27VolumesSmoke') }
|
||||
|
||||
tasks.register('cavesRelief28Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
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') }
|
||||
|
||||
+12
-8
@@ -44,7 +44,9 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
var admitted = new ArrayList<ExpansionIsland>();
|
||||
try {
|
||||
report.put("stage", "real admission toward an unseen satellite");
|
||||
admitted.add(admit(helper, level, generator, session, plan, true, report));
|
||||
if (!requests(level.getSeed(), session.islands(), plan, true).isEmpty())
|
||||
admitted.add(admit(helper, level, generator, session, plan, true, report));
|
||||
else report.put("collision_admission", "No raw expansion candidate reaches the close sites; initial island ownership already excludes them");
|
||||
report.put("stage", "real admission through an unclaimed interval");
|
||||
admitted.add(admit(helper, level, generator, session, plan, false, report));
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
@@ -88,15 +90,17 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
if (!tickets[0]) {
|
||||
requested.forEach(chunk -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, chunk, 2)); tickets[0] = true;
|
||||
}
|
||||
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks toward the void");
|
||||
flowTicks[0] += 20; report.put("flow_wait_ticks", flowTicks[0]); report.put("stage", "native fluid ticks beneath the suspended island");
|
||||
var source = AerialGeometry24.waterfallSource(waterfall);
|
||||
helper.assertTrue(level.getFluidState(source).is(FluidTags.WATER) && level.getFluidState(source).isSource(), "The authored source remains a single native water source");
|
||||
var below = waterAt(level, waterfall, waterfall.minY() - 16);
|
||||
var low = waterAt(level, waterfall, 8);
|
||||
// Alpha28 islets stand above real terrain: a cascade may land on Sanctuary.
|
||||
// Require native flow through the free air gap, not an impossible route through rock.
|
||||
var below = waterAt(level, waterfall, waterfall.minY() - 1);
|
||||
var low = waterAt(level, waterfall, waterfall.minY() - AerialPlanner24.MIN_ISLET_GAP);
|
||||
report.put("water_below_islet", below == null ? List.of() : coords(below));
|
||||
report.put("water_near_y0", low == null ? List.of() : coords(low));
|
||||
report.put("water_across_air_gap", low == null ? List.of() : coords(low));
|
||||
if (below == null || low == null) {
|
||||
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall below the island and down to y=8");
|
||||
helper.assertTrue(flowTicks[0] <= 4000, "Scheduled vanilla fluid ticks carry the waterfall through the sixteen-block air gap");
|
||||
helper.runAtTickTime(helper.getTick() + 20, () -> step[0].run()); return;
|
||||
}
|
||||
helper.assertTrue(!level.getFluidState(low).isSource(), "Low waterfall water is flowing, not a fabricated source column");
|
||||
@@ -128,7 +132,7 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
helper.assertTrue(elapsed <= 31_000, "Actual Session.create keeps its production thirty-second deadline");
|
||||
helper.assertTrue(!plan.conflicts(reserved), "The admitted expansion cannot claim any satellite chunk");
|
||||
helper.assertTrue(!ExpansionVoidReuse.requiresReload(level, reserved.id()), "Untouched candidate prepares in the same development session");
|
||||
if (!collision) helper.assertTrue(inAerialBand(reserved, plan), "The admitted free interval lies among the nearby satellites, not beyond a blanket exclusion ring");
|
||||
|
||||
helper.assertTrue(session.create(request.id(), "sanctuary", request.direction(), "temperate", request.diameter(), "natural").equals(reserved),
|
||||
"Reservation retry is idempotent");
|
||||
attempts.add(Map.of("request", request, "reserved", reserved, "elapsed_ms", elapsed));
|
||||
@@ -149,7 +153,7 @@ public final class Aerial24FlowAdmissionGameTests {
|
||||
var raw = ExpansionPlacement.candidates(seed, islands, id, "sanctuary", direction, "temperate", 128, "natural", 0);
|
||||
if (raw.isEmpty() || plan.conflicts(raw.getFirst()) != collision) continue;
|
||||
var filtered = raw.stream().filter(candidate -> !plan.conflicts(candidate)).findFirst().orElse(null);
|
||||
if (filtered == null || !collision && !inAerialBand(filtered, plan)) continue;
|
||||
if (filtered == null) continue;
|
||||
result.add(new Request(id, direction, 128, raw.getFirst(), filtered));
|
||||
}
|
||||
return result;
|
||||
|
||||
+54
-10
@@ -112,6 +112,12 @@ public final class Aerial24WorldGameTests {
|
||||
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true);
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
|
||||
}
|
||||
if (!report.containsKey("native_departures_ready")) {
|
||||
// Fail on a missing native start immediately instead of waiting for entities
|
||||
// which could never have been placed (for example, an obsolete void-only gate).
|
||||
report.put("stage", "verify saved aerial departures before entity loading");
|
||||
report.put("native_departures_ready", starts(helper, level, plan).size());
|
||||
}
|
||||
if (!tickets[0]) {
|
||||
entityChunks.forEach(pos -> level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, pos, 2)); tickets[0] = true;
|
||||
}
|
||||
@@ -170,14 +176,25 @@ public final class Aerial24WorldGameTests {
|
||||
var root = generator.source().islands().stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var views = new LinkedHashMap<String, Object>();
|
||||
for (var site : plan.sites()) {
|
||||
helper.assertTrue(site.claimedChunks().stream().noneMatch(chunk -> root.ownsChunk(chunk.x(), chunk.z())), "No aerial claim replaces natural starter chunks");
|
||||
if (site.merchant()) {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
helper.assertTrue(gap >= 16 && gap <= 48, "Native ship is just off its verified shore");
|
||||
helper.assertTrue(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4, "Boarding deck follows the nearby shore height");
|
||||
} else {
|
||||
helper.assertTrue(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islet floats above Sanctuary terrain");
|
||||
var column = generator.aerialBaseColumn24(site.shoreX(), site.shoreZ());
|
||||
helper.assertTrue(column.getBlock(site.shoreY() - 1).isSolid(), "Overhead islet retains a real ground witness below it");
|
||||
int highest = 0;
|
||||
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) { highest = y; break; }
|
||||
helper.assertTrue(site.minY() - highest - 1 >= AerialPlanner24.MIN_ISLET_GAP, "At least sixteen air blocks separate ground and suspended rock");
|
||||
}
|
||||
var overlap = new ExpansionIsland("aerial_overlap", root.id(), "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
|
||||
helper.assertTrue(plan.conflicts(overlap), "An unseen site already prevents overlapping expansion ownership");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
helper.assertTrue(distance <= 512, "Aerial site remains within 512 blocks of a real shore witness");
|
||||
views.put(site.id(), Map.of("shore", List.of(site.shoreX(), site.shoreY() + 2, site.shoreZ()),
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance,
|
||||
"entry", coordinates(AerialGeometry24.entry(site)), "centre", coordinates(site.locator()), "shore_distance", distance, "hull_to_shore", AerialPlanner24.shoreDistance(site),
|
||||
"exterior_view", List.of(site.minX() - 24, site.minY() + site.height() / 2, site.minZ() - 24)));
|
||||
}
|
||||
helper.assertTrue(!plan.conflicts(new ExpansionIsland("aerial_far", root.id(), "east", "temperate", 64, 4096, 4096, 42, "natural", "reserved", 24)),
|
||||
@@ -223,8 +240,17 @@ public final class Aerial24WorldGameTests {
|
||||
for (int dy = -2; dy <= 20; dy++) column.put(shore.getY() + dy, level.getBlockState(shore.above(dy)).toString());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column));
|
||||
helper.assertTrue(level.getBlockState(shore.below()).isSolid(), "Saved shore witness has a real FULL terrain support: " + site.id() + " " + shore);
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
if (site.merchant()) {
|
||||
helper.assertTrue(level.getBlockState(shore.above()).getCollisionShape(level, shore.above()).isEmpty(),
|
||||
"Shore viewpoint is not buried inside the island: " + site.id() + " " + shore.above() + " " + level.getBlockState(shore.above()));
|
||||
} else {
|
||||
// Natural trees may occupy the mathematical ground reference below an islet.
|
||||
// Observe a real nearby walking position without removing that vegetation.
|
||||
var viewpoint = nearbyGroundView(level, shore);
|
||||
helper.assertTrue(viewpoint != null, "A walkable ground viewpoint exists within twelve horizontal blocks below the suspended islet: " + site.id());
|
||||
shores.put(site.id(), Map.of("position", coordinates(shore), "column", column,
|
||||
"walkable_viewpoint", coordinates(viewpoint), "reference_obstruction", level.getBlockState(shore.above()).toString()));
|
||||
}
|
||||
var chestAt = AerialGeometry24.chest(site);
|
||||
helper.assertTrue(level.getBlockEntity(chestAt) instanceof RandomizableContainerBlockEntity, "Real chest at " + site.id()); chestCount++;
|
||||
var chest = (RandomizableContainerBlockEntity) level.getBlockEntity(chestAt);
|
||||
@@ -246,12 +272,15 @@ public final class Aerial24WorldGameTests {
|
||||
var source = AerialGeometry24.waterfallSource(site);
|
||||
helper.assertTrue(level.getFluidState(source).isSource(), "The one real source drives the native waterfall"); waterSources++;
|
||||
}
|
||||
// These columns are beyond the authored box and well outside initial terrain ownership.
|
||||
for (var pos : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2), new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
for (int y = site.minY(); y <= site.maxY() + 2; y++) {
|
||||
var state = level.getBlockState(pos.atY(y));
|
||||
helper.assertTrue(state.isAir() || site.waterfall() && !state.getFluidState().isEmpty(), "No manufactured structure leaks outside its authored footprint");
|
||||
}
|
||||
// Root terrain and its own trees may now exist outside these aerial envelopes.
|
||||
// Chunk clipping is checked against every authored position; do not confuse legal
|
||||
// surrounding terrain with a structure leak by requiring a whole exterior air column.
|
||||
helper.assertTrue(model.keySet().stream().allMatch(site.bounds()::isInside), "Every proposed structure block is inside its finite envelope");
|
||||
var exterior = new ArrayList<Map<String, Object>>();
|
||||
for (var at : List.of(new BlockPos(site.minX() - 2, site.minY(), site.minZ() - 2),
|
||||
new BlockPos(site.maxX() + 2, site.minY(), site.maxZ() + 2)))
|
||||
exterior.add(Map.of("position", coordinates(at), "observed_block", level.getBlockState(at).toString()));
|
||||
report.put(site.id() + "_exterior_observations", exterior);
|
||||
}
|
||||
helper.assertTrue(chestCount == 4 && waterSources == 1, "Four physical chests and exactly one planned source");
|
||||
report.put("chests", chestCount); report.put("waterfall_sources", waterSources); report.put("blocks_before", countSites(level, plan));
|
||||
@@ -259,6 +288,21 @@ public final class Aerial24WorldGameTests {
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static BlockPos nearbyGroundView(ServerLevel level, BlockPos reference) {
|
||||
for (int radius = 0; radius <= 12; radius++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != radius || Math.hypot(dx, dz) > 12) continue;
|
||||
for (int dy = 8; dy >= -8; dy--) {
|
||||
var at = reference.offset(dx, dy, dz);
|
||||
var floor = level.getBlockState(at.below());
|
||||
if (!floor.isSolid() || floor.is(net.minecraft.tags.BlockTags.LEAVES) || floor.is(net.minecraft.tags.BlockTags.LOGS)) continue;
|
||||
if (level.getBlockState(at).getCollisionShape(level, at).isEmpty()
|
||||
&& level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()
|
||||
&& level.getFluidState(at).isEmpty() && level.getFluidState(at.above()).isEmpty()) return at;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> vegetation(GameTestHelper helper, ServerLevel level, AerialPlan24.Site site) {
|
||||
helper.assertTrue(site.rockHeight() >= 20 && site.rockHeight() <= 35
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionStructures23;
|
||||
import fr.koka.sanctuary.expansion.SwampHutPlacement28;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusMemory;
|
||||
import fr.koka.sanctuary.knowledge.structures.StructureCensusService;
|
||||
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.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.SpawnerBlockEntity;
|
||||
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;
|
||||
|
||||
/** Observes normal root generation only. No start injection, fake room, expansion or seed search. */
|
||||
public final class Alpha28NativeStructuresGameTests {
|
||||
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path OUT = Path.of("diagnostics/native28");
|
||||
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void actualNativeStructures(GameTestHelper helper) throws Exception {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
try {
|
||||
var level = helper.getLevel(); var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", host.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("generate_structures", level.structureManager().shouldGenerateStructures());
|
||||
if (!level.structureManager().shouldGenerateStructures()) {
|
||||
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(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>();
|
||||
// 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());
|
||||
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.
|
||||
for (var candidate : candidates) {
|
||||
if (full.size() >= 16) break;
|
||||
full.add(candidate.chunk());
|
||||
}
|
||||
helper.assertTrue(full.size() <= 24, "Bounded native structure witness requests at most24 FULL chunks");
|
||||
var iterator = full.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;
|
||||
}
|
||||
inspect(helper, level, found, full, report);
|
||||
report.put("passed", true); write(report); helper.succeed();
|
||||
} catch (Exception error) { failure(report, error); throw new IllegalStateException("Native28 witness failed", error); }
|
||||
};
|
||||
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
|
||||
} catch (Exception error) { failure(report, error); throw error; }
|
||||
}
|
||||
|
||||
private static void inspect(GameTestHelper helper, ServerLevel level, Map<String, StructureStart> found,
|
||||
Set<ChunkPos> full, Map<String, Object> report) {
|
||||
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)
|
||||
.getStartForStructure(registry.getValue(Identifier.parse(entry.getKey())));
|
||||
helper.assertTrue(actual != null && actual.isValid(), "The real selected native start survives FULL: " + entry.getKey());
|
||||
var tag = actual.createTag(context, cp);
|
||||
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() - 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)) {
|
||||
var state = level.getBlockState(pier);
|
||||
helper.assertTrue(state.is(Blocks.OAK_LOG) || state.isSolid(), "A native hut pier has wood or a real solid foot: " + pier);
|
||||
}
|
||||
}
|
||||
if (entry.getKey().contains("mineshaft")) helper.assertTrue(counts.entrySet().stream()
|
||||
.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(),
|
||||
"corridor_flags", corridorFlags, "spawners", spawners));
|
||||
}
|
||||
var rooms = new ArrayList<Object>();
|
||||
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);
|
||||
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());
|
||||
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", "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");
|
||||
}
|
||||
private static void failure(Map<String, Object> report, Exception error) {
|
||||
report.put("passed", false); report.put("error", error.toString());
|
||||
try { Files.createDirectories(OUT); Files.writeString(OUT.resolve("failure.json"), JSON.toJson(report) + "\n"); }
|
||||
catch (Exception io) { error.addSuppressed(io); }
|
||||
SanctuaryMod.LOGGER.error("Native28 witness failed", error);
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
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.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Bounded comparison of the real registered density, followed by inspection of ordinary FULL chunks. */
|
||||
public final class Caves28WorldGameTests {
|
||||
@GameTest(maxTicks = 3600)
|
||||
public void geologyAndNativeOres(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var binding = generator.starterContext();
|
||||
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.geology28(), "Current world uses the native-cave adaptation");
|
||||
var current = binding.random().getSampler(field);
|
||||
var previous = binding.random().getSampler(field.legacy27());
|
||||
var context = SamplerContext.builder().enableCaches().build();
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
|
||||
report.put("scope", "Registered density comparison; no synthetic terrain, water, ore or room is placed by the test");
|
||||
var heights = new ArrayList<List<Integer>>();
|
||||
long oldSolid = 0, remainingSolid = 0, excavated = 0, restored = 0;
|
||||
int min = -Math.min(320, generator.initialDiameter() / 2), max = -min, raised = 0, lowered = 0, largestUplift = 0, largestSubsidence = 0;
|
||||
for (int z = min; z <= max; z += 32) for (int x = min; x <= max; x += 32) {
|
||||
int oldTop = 0, newTop = 0;
|
||||
for (int y = 2; y <= 382; y += 2) {
|
||||
boolean old = previous.sampleValue(context, x, y, z) > 0;
|
||||
boolean now = current.sampleValue(context, x, y, z) > 0;
|
||||
if (old) oldTop = y; if (now) newTop = y;
|
||||
if (y >= 48 && y <= 202) {
|
||||
if (old) { oldSolid++; if (now) remainingSolid++; else excavated++; }
|
||||
else if (now) restored++;
|
||||
}
|
||||
if (Math.hypot(x, z) < 40 && y >= 240)
|
||||
helper.assertTrue(old == now, "High arrival neighbourhood remains unchanged");
|
||||
}
|
||||
if (newTop > oldTop && oldTop > 208) { raised++; largestUplift = Math.max(largestUplift, newTop - oldTop); }
|
||||
if (newTop < oldTop && oldTop > 240) { lowered++; largestSubsidence = Math.max(largestSubsidence, oldTop - newTop); }
|
||||
heights.add(List.of(x, z, oldTop, newTop));
|
||||
}
|
||||
report.put("heights", heights); report.put("old_solid_samples", oldSolid);
|
||||
report.put("remaining_solid_samples", remainingSolid); report.put("excavated_samples", excavated);
|
||||
report.put("restored_rift_samples", restored); report.put("raised_columns", raised);
|
||||
report.put("largest_uplift", largestUplift);
|
||||
report.put("lowered_columns", lowered); report.put("largest_subsidence", largestSubsidence);
|
||||
helper.assertTrue(excavated > 0 && remainingSolid > excavated, "New passages exist while most sampled former rock remains");
|
||||
helper.assertTrue(raised > 0, "Native volumetric deformation raises some uplands");
|
||||
var childField = (PopulationIslandDensity) level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
var child = new fr.koka.sanctuary.expansion.ExpansionIsland("geology_witness", "sanctuary", "east", "temperate",
|
||||
512, 2048, 0, level.getSeed(), "natural", "ready", 24);
|
||||
var childSettings = level.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"))).value();
|
||||
var childState = net.minecraft.world.level.levelgen.RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), child.seed(), childSettings);
|
||||
var childNew = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField, child));
|
||||
var childOld = childState.getSampler(new fr.koka.sanctuary.expansion.ExpansionDensity(childField.legacy27(), child));
|
||||
long childExcavated = 0, childRemaining = 0;
|
||||
for (int z = -160; z <= 160; z += 32) for (int dx = -160; dx <= 160; dx += 32)
|
||||
for (int y = 48; y <= 202; y += 4) {
|
||||
if (childOld.sampleValue(context, 2048 + dx, y, z) > 0) {
|
||||
if (childNew.sampleValue(context, 2048 + dx, y, z) <= 0) childExcavated++;
|
||||
else childRemaining++;
|
||||
}
|
||||
}
|
||||
report.put("expansion_density", Map.of("diameter", 512, "excavated", childExcavated,
|
||||
"remaining", childRemaining, "reserved_or_opened", false));
|
||||
helper.assertTrue(childExcavated > 0 && childRemaining > childExcavated, "The actual expansion wrapper uses the same finite cave field");
|
||||
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 = 382; y >= 0; y -= 2) {
|
||||
var a = new StringBuilder(); var b = new StringBuilder();
|
||||
for (int x = min; x <= max; x += 2) {
|
||||
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", min, "step", 2));
|
||||
}
|
||||
report.put("sections", sections);
|
||||
var ores = new TreeMap<String, Integer>(); var oreDepths = new TreeMap<String, List<Integer>>();
|
||||
for (int cx : new int[]{-6, 0, 6}) for (int cz : new int[]{-6, 0, 6}) {
|
||||
var chunk = level.getChunk(cx, cz, ChunkStatus.FULL, true);
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) for (int y = 1; y < 383; y++) {
|
||||
var state = chunk.getBlockState(new BlockPos(cx * 16 + x, y, cz * 16 + z));
|
||||
String id = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
if (id.endsWith("_ore")) {
|
||||
ores.merge(id, 1, Integer::sum);
|
||||
var band = oreDepths.computeIfAbsent(id, ignored -> new ArrayList<>(List.of(384, 0)));
|
||||
band.set(0, Math.min(band.get(0), y)); band.set(1, Math.max(band.get(1), y));
|
||||
}
|
||||
}
|
||||
}
|
||||
report.put("full_chunks_inspected", 9); report.put("ores", ores); report.put("ore_y_ranges", oreDepths);
|
||||
helper.assertTrue(!ores.isEmpty(), "Actual generated rock contains native ore deposits");
|
||||
report.put("passed", true);
|
||||
Path path = Path.of("diagnostics/alpha28/geology.json"); Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -23,7 +23,7 @@ import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
|
||||
/** Compares the explicit legacy27 mode against frozen alpha10, not the intentional alpha28 terrain changes. */
|
||||
final class PopulationDensityEquivalenceDiagnostics {
|
||||
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
|
||||
|
||||
@@ -39,7 +39,7 @@ final class PopulationDensityEquivalenceDiagnostics {
|
||||
var key = SanctuaryMod.id("population_" + players);
|
||||
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
|
||||
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
|
||||
var density = (PopulationIslandDensity) registered;
|
||||
var density = ((PopulationIslandDensity) registered).legacy27();
|
||||
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
|
||||
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
|
||||
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
|
||||
@@ -113,7 +113,7 @@ final class PopulationDensityEquivalenceDiagnostics {
|
||||
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
|
||||
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
|
||||
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
|
||||
"scope", "Explicit legacy27 mode versus frozen alpha10: real compiled scalar/bulk densities, 4 capacities x 3 seeds, signed coordinates, region/coast/height boundaries, context switches and concurrency. Geology28 is intentionally disabled only for this historical equivalence proof; no alpha28 generation claim.")) + "\n");
|
||||
}
|
||||
|
||||
private static List<DensityVolume> volumes(IslandCapacity capacity) {
|
||||
|
||||
+5
-1
@@ -87,6 +87,7 @@ public final class PopulationDiagnostics {
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_" + capacity.players()))).value();
|
||||
helper.assertTrue(density instanceof PopulationIslandDensity, "Inspect the actual compiled population density");
|
||||
var island = (PopulationIslandDensity) density;
|
||||
var legacyEnvelope = island.legacy27();
|
||||
helper.assertTrue(island.capacity() == capacity, "Noise settings and density must agree on saved capacity");
|
||||
var random = binding.random();
|
||||
var replay = RandomState.create(level.registryAccess().lookupOrThrow(Registries.NOISE), random.seed(), generator.generatorSettings().value());
|
||||
@@ -102,7 +103,10 @@ public final class PopulationDiagnostics {
|
||||
float value = random.sampleBlockValueUncached(density, x, y, z);
|
||||
float raw = random.sampleBlockValueUncached(island.terrain(), x, y - 64, z);
|
||||
helper.assertTrue(Float.isFinite(value), "Density remains finite at every sampled position");
|
||||
helper.assertTrue(value <= 0 || raw > 0, "Enlarging the envelope must never invent terrain inside natural holes");
|
||||
float historical = island.geology28()
|
||||
? random.sampleBlockValueUncached(legacyEnvelope, x, y, z) : value;
|
||||
helper.assertTrue(historical <= 0 || raw > 0,
|
||||
"The explicit legacy27 envelope never fills its raw natural holes; alpha28 uplands change sample Y");
|
||||
if (value > 0) { if (bottom < 0) bottom = y; top = y; }
|
||||
if (x % (step * 8) == 0 && z % (step * 8) == 0)
|
||||
helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(replay.sampleBlockValueUncached(density, x, y, z)),
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
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.river30(binding.generator(),binding.random());
|
||||
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());
|
||||
helper.assertTrue(plan.features().stream().anyMatch(f->f.kind()==PopulationHydrology.Kind.RIVER),"An island-wide river must be present in the release witness");
|
||||
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().limit(2).toList();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())).isEmpty(),"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)).isEmpty()){flowing++;}
|
||||
if(!falls.isEmpty())helper.assertTrue(flowing>0,"Vanilla fluid ticks carry water below at least one opened bank");
|
||||
report.put("full_chunks",chunks.size());report.put("flowing_below_openings",flowing);report.put("passed",true);
|
||||
Path out=Path.of("diagnostics/alpha30/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();
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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 fineLedgesPreserveTheAcceptedMass(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.relief30() && !field.terraces29(), "Alpha30 localizes deformation and disables periodic micro-ledges");
|
||||
var random = generator.starterContext().random();
|
||||
var current = random.getSampler(field); var previous = random.getSampler(field.legacy28());
|
||||
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 <= 180; y += 13) {
|
||||
helper.assertTrue(Float.floatToIntBits(current.sampleValue(context, x, y, z))
|
||||
== Float.floatToIntBits(previous.sampleValue(context, x, y, z)), "Lower density is exactly alpha28");
|
||||
lowerSamples++;
|
||||
}
|
||||
for (int y = 181; y <= 320; 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 && unchanged > changed * 3L, "Localized relief retains most of the accepted upper mass");
|
||||
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 = 305; y >= 205; 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", 305, "step", 1));
|
||||
}
|
||||
report.put("sections", sections); report.put("passed", true);
|
||||
Path path = Path.of("diagnostics/alpha30", "relief-" + generator.initialDiameter() + "-" + level.getSeed() + ".json");
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -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)),
|
||||
|
||||
+130
@@ -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.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"],
|
||||
|
||||
+10
-2
@@ -146,9 +146,17 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
@Override public void applyBiomeDecoration(net.minecraft.world.level.WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
var island = islandAt(chunk.getPos().getMiddleBlockX(), chunk.getPos().getMiddleBlockZ());
|
||||
if (!ExpansionOcean23.active(island)) { super.applyBiomeDecoration(level, chunk, structures); return; }
|
||||
fr.koka.sanctuary.worldgen.IslandOres28.place(level, this, chunk, island);
|
||||
if (!ExpansionOcean23.active(island)) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
|
||||
return;
|
||||
}
|
||||
Integer previous = ExpansionOcean23.enterDecoration(ExpansionOcean23.seaLevel(island));
|
||||
try { super.applyBiomeDecoration(level, chunk, structures); }
|
||||
try {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
fr.koka.sanctuary.worldgen.CavityMonsterRoom28.decorate(level, this, chunk);
|
||||
}
|
||||
finally { ExpansionOcean23.restoreDecoration(previous); }
|
||||
}
|
||||
@Override public int getSpawnHeight(LevelHeightAccessor level) { return 220; }
|
||||
|
||||
@@ -49,7 +49,11 @@ public final class ExpansionRelief {
|
||||
public static float sculpt(ExpansionIsland island, Column column, double y, float source) {
|
||||
if (source <= 0) return source;
|
||||
double reference = referenceY(island, y);
|
||||
double canyon = 2.1 * column.canyonWeight() * smoothstep((reference - 120.0) / 34.0);
|
||||
double canyonHeight = island.generation() == 24
|
||||
? smoothstep((reference - 126 - column.uplift() * .35) / 16)
|
||||
* smoothstep((226 + column.uplift() * .35 - reference) / 18)
|
||||
: smoothstep((reference - 120.0) / 34.0);
|
||||
double canyon = 2.1 * column.canyonWeight() * canyonHeight;
|
||||
double crater = 2.1 * column.craterWeight() * smoothstep((reference - CRATER_FLOOR) / 14.0);
|
||||
return (float) Math.clamp(source - Math.max(canyon, crater), -1.0, 1.0);
|
||||
}
|
||||
|
||||
+108
-21
@@ -169,6 +169,15 @@ public final class ExpansionStructures23 {
|
||||
if (centre.top < 32) return null;
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var resolver = view.getBiomeSource().createResolver(sampler);
|
||||
var hutHeights = new ArrayList<Integer>();
|
||||
// The biome is genuinely three dimensional: a swamp bank can be below another island floor.
|
||||
for (int y = Math.min(centre.top + 1, 280); y >= 48 && hutHeights.size() < 4; y--) {
|
||||
var floor = centre.column.getBlock(y - 1);
|
||||
if ((!floor.isSolid() && !floor.getFluidState().is(net.minecraft.tags.FluidTags.WATER))
|
||||
|| !centre.column.getBlock(y).isAir() || !centre.column.getBlock(y + 1).isAir()) continue;
|
||||
if (!host.structureBiome(resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2)).is(net.minecraft.world.level.biome.Biomes.SWAMP)) continue;
|
||||
if (hutHeights.isEmpty() || hutHeights.getLast() - y >= 8) hutHeights.add(y);
|
||||
}
|
||||
int undergroundY = Math.clamp(centre.top - 60, 50, 210);
|
||||
Holder<Biome> surfaceBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, centre.top >> 2, z >> 2));
|
||||
Holder<Biome> undergroundBiome = host.structureBiome(resolver.getNoiseBiome(x >> 2, undergroundY >> 2, z >> 2));
|
||||
@@ -182,7 +191,8 @@ public final class ExpansionStructures23 {
|
||||
if (holder == null) continue;
|
||||
Holder<Biome> biome = entry.habitat() == Habitat.UNDERGROUND ? undergroundBiome
|
||||
: entry.habitat() == Habitat.OCEAN ? waterBiome : surfaceBiome;
|
||||
if (!holder.value().biomes().contains(biome)) continue;
|
||||
if (entry.family().equals("swamp_hut")) { if (hutHeights.isEmpty()) continue; }
|
||||
else if (!holder.value().biomes().contains(biome)) continue;
|
||||
if (entry.habitat() == Habitat.OCEAN && !ExpansionOcean23.column(island, x, z).water()) continue;
|
||||
long salt = mix(candidate.roll() ^ entry.id().hashCode() * 0x9e3779b97f4a7c15L);
|
||||
double unit = ((salt >>> 11) + 1.0) * 0x1.0p-53;
|
||||
@@ -196,10 +206,17 @@ public final class ExpansionStructures23 {
|
||||
checkDeadline(deadlineNanos);
|
||||
Structure original = choice.holder().value();
|
||||
Entry entry = choice.entry();
|
||||
int variants = entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
|
||||
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 = undergroundY - vertical * 20;
|
||||
int targetY = hut ? hutHeights.get(vertical) : undergroundY - vertical * 20;
|
||||
ExpansionChunkGenerator nativeView = hut && view instanceof NativeView local
|
||||
? local.atHeight(targetY + 1) : view;
|
||||
GroundSampler nativeGround = hut ? new GroundSampler(nativeView, chunk, random, deadlineNanos, targetY + 1) : ground;
|
||||
Structure generated = original;
|
||||
if (entry.family().equals("trial_chambers")) generated = withStartHeight(original, registries, targetY);
|
||||
Predicate<Holder<Biome>> valid = b -> original.biomes().contains(host.structureBiome(b));
|
||||
@@ -207,18 +224,28 @@ public final class ExpansionStructures23 {
|
||||
if (original instanceof StrongholdStructure) {
|
||||
// Vanilla stronghold piece weights are static mutable state.
|
||||
synchronized (StrongholdStructure.class) {
|
||||
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), candidate.chunk(), 0, chunk, valid);
|
||||
}
|
||||
} else {
|
||||
start = generated.generate(choice.holder(), dimension, registries, view, view.getBiomeSource(), sampler,
|
||||
start = generated.generate(choice.holder(), dimension, registries, nativeView, view.getBiomeSource(), sampler,
|
||||
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, ground);
|
||||
String rejection = supportRejection(host, island, entry, start, ground);
|
||||
start = adjustNativeStart(original, start, entry, island, targetY, nativeGround);
|
||||
String rejection = supportRejection(host, island, entry, start, nativeGround);
|
||||
if (rejection != null) { diagnostic(island, candidate, rejection, entry.id()); continue; }
|
||||
((NativeStructureMetadata23) (Object) start).sanctuary$native23(new NativeStructureMetadata23.Data(island.seed(), seaLevel));
|
||||
return new Accepted(choice.holder(), start);
|
||||
@@ -308,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));
|
||||
@@ -329,21 +361,54 @@ 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";
|
||||
if (island.origin()) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
if (entry.family().equals("swamp_hut")) {
|
||||
if (host instanceof SanctuaryChunkGenerator sanctuary && island.origin()) {
|
||||
var binding = sanctuary.starterContext();
|
||||
var box = new BoundingBox(total.minX(), total.minY() - SwampHutPlacement28.MAX_PIER_DROP - 2,
|
||||
total.minZ(), total.maxX(), total.maxY(), total.maxZ());
|
||||
for (var region : fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.plansIntersecting(
|
||||
binding.generator(), binding.random(), total.minX() - 16, total.minZ() - 16, total.maxX() + 16, total.maxZ() + 16)) {
|
||||
String refusal = SwampHutPlacement28.hydrologyRejection(region, box);
|
||||
if (refusal != null) return refusal;
|
||||
}
|
||||
}
|
||||
return SwampHutPlacement28.rejection(start, (x, z) -> ground.at(x, z).column);
|
||||
}
|
||||
boolean underground = entry.habitat() == Habitat.UNDERGROUND || undergroundPortal(start);
|
||||
boolean aquatic = entry.habitat() == Habitat.OCEAN;
|
||||
int siteMin = Integer.MAX_VALUE, siteMax = Integer.MIN_VALUE;
|
||||
for (StructurePiece piece : start.getPieces()) {
|
||||
BoundingBox box = piece.getBoundingBox();
|
||||
if (entry.family().equals("mineshaft")
|
||||
&& piece instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftPieces.MineShaftCorridor) {
|
||||
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;
|
||||
boolean buriedPiece = underground || !aquatic && box.maxY() < ground.at(box.getCenter().getX(), box.getCenter().getZ()).top - 6;
|
||||
int step = buriedPiece ? 8 : 4;
|
||||
@@ -372,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";
|
||||
@@ -474,7 +544,7 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
private static int weight(String family) {
|
||||
return switch (family) { case "camp" -> 6; case "ruined_portal", "mineshaft", "ocean_ruin" -> 3;
|
||||
case "village", "shipwreck", "trail_ruins", "buried_treasure" -> 2; default -> 1; };
|
||||
case "village", "shipwreck", "trail_ruins", "buried_treasure", "swamp_hut" -> 2; default -> 1; };
|
||||
}
|
||||
private static int[] samples(int low, int high, int step) {
|
||||
var values = new java.util.TreeSet<Integer>(); values.add(low); values.add(high); values.add((low + high) / 2);
|
||||
@@ -493,9 +563,11 @@ public final class ExpansionStructures23 {
|
||||
private record Choice(Entry entry, Holder<Structure> holder, double score) {}
|
||||
private static final class Ground {
|
||||
final NoiseColumn column; final int top; final int thickness;
|
||||
Ground(NoiseColumn column, int min, int max) {
|
||||
Ground(NoiseColumn column, int min, int max) { this(column, min, max, false); }
|
||||
Ground(NoiseColumn column, int min, int max, boolean waterSurface) {
|
||||
this.column = column; int y = max;
|
||||
while (y >= min && !column.getBlock(y).isSolid()) y--;
|
||||
while (y >= min && !column.getBlock(y).isSolid()
|
||||
&& !(waterSurface && column.getBlock(y).getFluidState().is(net.minecraft.tags.FluidTags.WATER))) y--;
|
||||
top = y;
|
||||
while (y >= min && top - y < 24 && solid(y)) y--;
|
||||
thickness = top - y;
|
||||
@@ -505,17 +577,23 @@ public final class ExpansionStructures23 {
|
||||
}
|
||||
private static final class GroundSampler {
|
||||
final ExpansionChunkGenerator generator; final LevelHeightAccessor level; final RandomState random; final long deadline;
|
||||
final Integer localCeiling;
|
||||
final Map<Long, Ground> columns = new HashMap<>();
|
||||
fr.koka.sanctuary.worldgen.city.LostCityProtection16 localCity;
|
||||
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline) {
|
||||
this(generator, level, random, deadline, null);
|
||||
}
|
||||
GroundSampler(ExpansionChunkGenerator generator, LevelHeightAccessor level, RandomState random, long deadline, Integer localCeiling) {
|
||||
this.generator = generator; this.level = level; this.random = random; this.deadline = deadline;
|
||||
this.localCeiling = localCeiling;
|
||||
}
|
||||
Ground at(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
Ground old = columns.get(key); if (old != null) return old;
|
||||
checkDeadline(deadline);
|
||||
if (columns.size() >= MAX_COLUMN_SAMPLES) throw new SampleBudgetExceeded();
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(), level.getMaxY());
|
||||
Ground result = new Ground(generator.getBaseColumn(x, z, level, random), level.getMinY(),
|
||||
localCeiling == null ? level.getMaxY() : localCeiling, localCeiling != null);
|
||||
columns.put(key, result); return result;
|
||||
}
|
||||
}
|
||||
@@ -536,14 +614,23 @@ public final class ExpansionStructures23 {
|
||||
/** Isolated biome snapshot during preflight, per-island sea level during native generation. */
|
||||
private static final class NativeView extends ExpansionChunkGenerator {
|
||||
private final ExpansionChunkGenerator host; private final ExpansionIsland island;
|
||||
private Integer localCeiling;
|
||||
NativeView(ExpansionChunkGenerator host, ExpansionIsland island, ExpansionBiomeSource source, RegistryAccess registries) {
|
||||
super(source, host.initialDiameter(), true); this.host = host; this.island = island; this.registries = registries;
|
||||
}
|
||||
NativeView atHeight(int ceiling) {
|
||||
var result = new NativeView(host, island, source(), registries); result.localCeiling = ceiling; return result;
|
||||
}
|
||||
@Override public int rootGeneration() { return island.generation(); }
|
||||
@Override public Holder<Biome> structureBiome(Holder<Biome> biome) { return host.structureBiome(biome); }
|
||||
@Override public int getSeaLevel() { return ExpansionOcean23.active(island) ? ExpansionOcean23.seaLevel(island) : 192; }
|
||||
@Override public RandomState randomState(ExpansionIsland candidate) { return island.origin() ? host.randomState(candidate) : super.randomState(candidate); }
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
|
||||
if (localCeiling != null) {
|
||||
var column = getBaseColumn(x, z, level, random);
|
||||
for (int y = localCeiling; y >= level.getMinY(); y--) if (type.isOpaque().test(column.getBlock(y))) return y + 1;
|
||||
return level.getMinY();
|
||||
}
|
||||
return island.origin() ? host.getBaseHeight(x, z, type, level, random) : super.getBaseHeight(x, z, type, level, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState random) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
|
||||
/** Certifies the support positions actually used by vanilla MineShaftCorridor, not a percentage of nearby rock. */
|
||||
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}) {
|
||||
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());
|
||||
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()) {
|
||||
below = y; break;
|
||||
}
|
||||
}
|
||||
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
|
||||
for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.getFluidState().isEmpty()) break;
|
||||
if (state.isSolid()) {
|
||||
above = y; break;
|
||||
}
|
||||
}
|
||||
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 java.util.List.copyOf(volumes);
|
||||
}
|
||||
}
|
||||
+24
@@ -7,10 +7,34 @@ import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPiec
|
||||
/** Scoped native placement parameters; never change the sea level of another dimension or start. */
|
||||
public final class NativeStructurePlacement23 {
|
||||
private static final ThreadLocal<Integer> SEA_LEVEL = new ThreadLocal<>();
|
||||
public record HutScope(java.util.List<net.minecraft.core.BlockPos> piers,
|
||||
java.util.List<net.minecraft.world.level.levelgen.structure.BoundingBox> bodies) {}
|
||||
private static final ThreadLocal<HutScope> HUT = new ThreadLocal<>();
|
||||
private NativeStructurePlacement23() {}
|
||||
public static Integer seaLevel() { return SEA_LEVEL.get(); }
|
||||
public static Integer enter(int seaLevel) { Integer previous = SEA_LEVEL.get(); SEA_LEVEL.set(seaLevel); return previous; }
|
||||
public static void restore(Integer previous) { if (previous == null) SEA_LEVEL.remove(); else SEA_LEVEL.set(previous); }
|
||||
public static HutScope enterHut(net.minecraft.world.level.levelgen.structure.StructureStart start) {
|
||||
var previous = HUT.get();
|
||||
var pieces = start.getPieces().stream().filter(net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece.class::isInstance).toList();
|
||||
HUT.set(new HutScope(pieces.stream().flatMap(piece -> SwampHutPlacement28.piers(piece).stream()).toList(),
|
||||
pieces.stream().map(StructurePiece::getBoundingBox).toList()));
|
||||
return previous;
|
||||
}
|
||||
public static void restoreHut(HutScope previous) {
|
||||
if (previous == null) HUT.remove(); else HUT.set(previous);
|
||||
}
|
||||
public static boolean allowsHutWrite(net.minecraft.core.BlockPos pos, net.minecraft.world.level.block.state.BlockState state,
|
||||
net.minecraft.world.level.block.state.BlockState replaced) {
|
||||
var scope = HUT.get(); if (scope == null) return false;
|
||||
// Native construction in already empty air above a certified bank does not cut its retaining bed.
|
||||
if (replaced.isAir() && scope.bodies().stream().anyMatch(box -> box.isInside(pos))) return true;
|
||||
if (!state.is(net.minecraft.world.level.block.Blocks.OAK_LOG)
|
||||
|| !(replaced.isAir() || replaced.getFluidState().isSource()
|
||||
&& replaced.getFluidState().is(net.minecraft.tags.FluidTags.WATER))) return false;
|
||||
return scope.piers().stream().anyMatch(pier -> pier.getX() == pos.getX() && pier.getZ() == pos.getZ()
|
||||
&& pos.getY() <= pier.getY() && pos.getY() > pier.getY() - SwampHutPlacement28.MAX_PIER_DROP);
|
||||
}
|
||||
public static void move(StructurePiece piece, int dy) {
|
||||
if (piece instanceof OceanMonumentPieces.MonumentBuilding monument) {
|
||||
for (StructurePiece child : ((MonumentChildren23Accessor) monument).sanctuary$children()) child.move(0, dy, 0);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package fr.koka.sanctuary.expansion;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
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.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** Native hut geometry on a real shallow bank, including banks below another island floor. */
|
||||
public final class SwampHutPlacement28 {
|
||||
public static final int MAX_PIER_DROP = 8;
|
||||
private SwampHutPlacement28() {}
|
||||
|
||||
public static String rejection(StructureStart start, BiFunction<Integer, Integer, NoiseColumn> columns) {
|
||||
for (var piece : start.getPieces()) {
|
||||
if (!(piece instanceof SwampHutPiece)) return "hut-unexpected-piece";
|
||||
var box = piece.getBoundingBox();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++) {
|
||||
var column = columns.apply(x, z);
|
||||
int waterTop = Integer.MIN_VALUE, waterBottom = Integer.MAX_VALUE;
|
||||
for (int y = box.minY() - MAX_PIER_DROP; y <= box.maxY(); y++) {
|
||||
var state = column.getBlock(y); var fluid = state.getFluidState();
|
||||
if (y >= box.minY() + 2 && !state.isAir()) return "hut-natural-headroom";
|
||||
if (fluid.isEmpty()) continue;
|
||||
if (fluid.getType() != Fluids.WATER || !fluid.isSource()) return "hut-flowing-or-nonwater";
|
||||
waterTop = Math.max(waterTop, y); waterBottom = Math.min(waterBottom, y);
|
||||
}
|
||||
if (waterTop != Integer.MIN_VALUE) {
|
||||
if (waterTop > box.minY() || waterTop - waterBottom + 1 > 4) return "hut-deep-water";
|
||||
if (!solid(column.getBlock(waterBottom - 1)) || !solid(column.getBlock(waterBottom - 2))) return "hut-unsealed-bed";
|
||||
}
|
||||
}
|
||||
for (var pier : piers(piece)) {
|
||||
var column = columns.apply(pier.getX(), pier.getZ()); int y = pier.getY(), depth = 0;
|
||||
while (replaceable(column.getBlock(y)) && depth <= MAX_PIER_DROP) { y--; depth++; }
|
||||
if (depth > MAX_PIER_DROP || !solid(column.getBlock(y)) || !solid(column.getBlock(y - 1))) return "hut-unsupported-pier";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Still water is compatible; springs, spills, channels and lava keep their protection. */
|
||||
public static String hydrologyRejection(PopulationHydrologyRuntime.RegionPlan region, BoundingBox box) {
|
||||
for (var spring : region.water().springs()) if (intersects(box, spring.flowBounds(), 3)) return "hut-protected-spring";
|
||||
for (var spill : region.water().spills()) if (intersects(box, spill.flowBounds(), 3)) return "hut-protected-spill";
|
||||
for (var flow : region.lava().flowBounds()) if (intersects(box, flow, 4)) return "hut-protected-lava";
|
||||
var rivers = region.water().features().stream().filter(f -> f.kind() == PopulationHydrology.Kind.RIVER)
|
||||
.map(PopulationHydrology.Feature::id).collect(java.util.stream.Collectors.toSet());
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
for (var cell : region.water().cellsAt(x, z)) if (rivers.contains(cell.featureId())
|
||||
&& box.minY() <= cell.carveTop() && box.maxY() >= cell.bedY() - cell.sedimentDepth() - 1) return "hut-protected-river";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Exact four support origins used by SwampHutPiece.postProcess. */
|
||||
public static List<BlockPos> piers(StructurePiece piece) {
|
||||
var result = new ArrayList<BlockPos>(4);
|
||||
for (int z : new int[]{2, 7}) for (int x : new int[]{1, 5}) result.add(worldPosition(piece, x, -1, z));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
public static BlockPos worldPosition(StructurePiece piece, int x, int y, int z) {
|
||||
var b = piece.getBoundingBox(); Direction facing = piece.getOrientation();
|
||||
if (facing == null || facing.getAxis().isVertical()) throw new IllegalArgumentException("A hut requires horizontal orientation");
|
||||
int wx = switch (facing) { case WEST -> b.maxX() - z; case EAST -> b.minX() + z; default -> b.minX() + x; };
|
||||
int wz = switch (facing) { case NORTH -> b.maxZ() - z; case SOUTH -> b.minZ() + z; default -> b.minZ() + x; };
|
||||
return new BlockPos(wx, b.minY() + y, wz);
|
||||
}
|
||||
private static boolean solid(BlockState state) { return state.isSolid() && state.getFluidState().isEmpty(); }
|
||||
private static boolean replaceable(BlockState state) {
|
||||
return state.isAir() || state.liquid() || state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS);
|
||||
}
|
||||
private static boolean intersects(BoundingBox b, PopulationHydrology.FlowBounds flow, int margin) {
|
||||
return b.minX() <= flow.maxX() + margin && b.maxX() >= flow.minX() - margin
|
||||
&& b.minY() <= flow.maxY() + margin && b.maxY() >= flow.minY() - margin
|
||||
&& b.minZ() <= flow.maxZ() + margin && b.maxZ() >= flow.minZ() - margin;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -67,8 +67,9 @@ public abstract class NativeStructureStart23Mixin implements NativeStructureMeta
|
||||
RandomSource random, BoundingBox bounds, ChunkPos pos, Operation<Void> original) {
|
||||
if (sanctuary$native23 == null) { original.call(level, structures, generator, random, bounds, pos); return; }
|
||||
Integer previous = NativeStructurePlacement23.enter(sanctuary$native23.seaLevel());
|
||||
var previousHut = NativeStructurePlacement23.enterHut((StructureStart) (Object) this);
|
||||
try { original.call(level, structures, generator, random, bounds, pos); }
|
||||
finally { NativeStructurePlacement23.restore(previous); }
|
||||
finally { NativeStructurePlacement23.restoreHut(previousHut); NativeStructurePlacement23.restore(previous); }
|
||||
}
|
||||
|
||||
@Unique private static Data sanctuary$read(CompoundTag tag) {
|
||||
|
||||
+4
@@ -62,6 +62,10 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
}
|
||||
// The dedicated shoreline feature still grows cane on its naturally valid dry banks.
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Admitted native huts use existing air and four bounded wooden piers in still water.
|
||||
// The intact basin bed, all other water cells, island ownership and secrets remain protected.
|
||||
if (!generator.protectsCityDecoration(pos)
|
||||
&& fr.koka.sanctuary.expansion.NativeStructurePlacement23.allowsHutWrite(pos, state, region.getBlockState(pos))) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17 || generator.rootGeneration() == 19 || generator.rootGeneration() == 20 || generator.rootGeneration() == 21 || generator.rootGeneration() == 22 || (generator.rootGeneration() == 23 || generator.rootGeneration() == 24))
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Bounded domain deformation around native rock noise, followed by native cave cuts. */
|
||||
public final class CavesRelief28 {
|
||||
public static final int CAVE_OFFSET = 96;
|
||||
public static final int DEFORMATION_START = 180;
|
||||
public static final double MAX_HORIZONTAL_SHIFT = 16;
|
||||
public static final double MAX_VERTICAL_SHIFT = 40;
|
||||
private CavesRelief28() {}
|
||||
|
||||
public record Warp(double x, double y, double z) {}
|
||||
|
||||
public static double deformationWeight(int x, int y, int z, double radius) {
|
||||
double distance = Math.hypot((double) x, z);
|
||||
double arrival = smooth((distance - 48) / 48);
|
||||
double coast = 1 - smooth((distance / radius - .66) / .26);
|
||||
return smooth((y - DEFORMATION_START) / 48.0) * arrival * coast;
|
||||
}
|
||||
|
||||
/** Signed smooth displacements: no folded ridge, plateau saturation or height-map uplift. */
|
||||
public static Warp warp(double weight, float across, float vertical, float along) {
|
||||
return new Warp(MAX_HORIZONTAL_SHIFT * Math.tanh(across * 1.6) * weight,
|
||||
MAX_VERTICAL_SHIFT * Math.tanh(vertical * 1.6) * weight,
|
||||
MAX_HORIZONTAL_SHIFT * Math.tanh(along * 1.6) * weight);
|
||||
}
|
||||
|
||||
public static float carve(float rock, float nativeCaves, int y) {
|
||||
if (rock <= 0) return rock;
|
||||
double weight = smooth((y - 40) / 28.0) * (1 - smooth((y - 202) / 30.0));
|
||||
if (weight <= 0) return rock;
|
||||
return Math.min(rock, (float) Math.clamp(nativeCaves + (1 - weight) * 2, -1, 1));
|
||||
}
|
||||
private static double smooth(double value) {
|
||||
double t = Math.clamp(value, 0, 1);
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.MonsterRoomFeature;
|
||||
|
||||
/** A few attempts at real cave walls; the vanilla feature owns its shell, loot and spawner. */
|
||||
public final class CavityMonsterRoom28 {
|
||||
private static final MonsterRoomFeature NATIVE = new MonsterRoomFeature();
|
||||
private static final int MAX_ATTEMPTS = 8;
|
||||
private CavityMonsterRoom28() {}
|
||||
|
||||
public static void decorate(WorldGenLevel level, ChunkGenerator generator, ChunkAccess chunk) {
|
||||
if (!(generator instanceof ExpansionChunkGenerator host) || host.rootGeneration() != 24
|
||||
|| !level.getLevel().structureManager().shouldGenerateStructures()) return;
|
||||
var pos = chunk.getPos(); var island = host.islandForChunk(pos.x(), pos.z());
|
||||
if (island == null || island.generation() != 24) return;
|
||||
long seed = WoodlandGroveGeometry.seed(island.seed(), pos.x(), 28, pos.z(), 0xD086E028L);
|
||||
if (Math.floorMod(seed, 3) != 0) return;
|
||||
var candidates = new ArrayList<BlockPos>();
|
||||
int bottom = Math.max(level.getMinY() + 2, 32), top = Math.min(level.getMaxY() - 6, 280);
|
||||
for (int dx : new int[]{4, 11}) for (int dz : new int[]{4, 11}) {
|
||||
int x = pos.getMinBlockX() + dx, z = pos.getMinBlockZ() + dz;
|
||||
for (int y = bottom; y <= top; y++) {
|
||||
BlockPos at = new BlockPos(x, y, z);
|
||||
if (!level.getBlockState(at.below()).isSolid() || !level.getBlockState(at.above(4)).isSolid()) continue;
|
||||
boolean caveEdge = false;
|
||||
for (Direction side : Direction.Plane.HORIZONTAL) {
|
||||
BlockPos opening = at.relative(side, 4);
|
||||
if (level.getBlockState(opening).isAir() && level.getBlockState(opening.above()).isAir()) { caveEdge = true; break; }
|
||||
}
|
||||
if (caveEdge) candidates.add(at);
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.<BlockPos>comparingLong(at -> WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 28)));
|
||||
int attempts = 0;
|
||||
for (BlockPos at : candidates) {
|
||||
if (attempts++ >= MAX_ATTEMPTS) break;
|
||||
if (!safe(level, host, at, island.id())) continue;
|
||||
// No virtual solid floor/roof and no prebuilt room. Native rejection remains authoritative.
|
||||
if (NATIVE.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, at.getX(), at.getY(), at.getZ(), 0xC0128)), at)) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean safe(WorldGenLevel level, ExpansionChunkGenerator host, BlockPos at, String owner) {
|
||||
for (int dx = -4; dx <= 4; dx++) for (int dz = -4; dz <= 4; dz++) for (int dy = -1; dy <= 4; dy++) {
|
||||
BlockPos block = at.offset(dx, dy, dz);
|
||||
var destination = host.islandForChunk(block.getX() >> 4, block.getZ() >> 4);
|
||||
if (destination == null || !destination.id().equals(owner)) return false;
|
||||
var state = level.getBlockState(block);
|
||||
if (!state.getFluidState().isEmpty() || state.hasBlockEntity() || state.is(Blocks.BEDROCK)) return false;
|
||||
if (host instanceof SanctuaryChunkGenerator sanctuary
|
||||
&& (sanctuary.protectsDecoration(block) || sanctuary.protectsCityDecoration(block))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
|
||||
/** Native finite ore veins, positioned within the whole floating rock column rather than vanilla sea-level bands. */
|
||||
public final class IslandOres28 {
|
||||
private IslandOres28() {}
|
||||
private record Deposit(String feature, int attempts, double minDepth, double maxDepth) {}
|
||||
// Small supplementary populations shared by all climates. Existing biome ores remain in place.
|
||||
private static final Deposit[] DEPOSITS = {
|
||||
new Deposit("ore_coal", 8, .05, .65), new Deposit("ore_iron", 7, .12, .88),
|
||||
new Deposit("ore_copper_small", 6, .10, .66), new Deposit("ore_gold", 2, .52, .90),
|
||||
new Deposit("ore_redstone", 3, .56, .92), new Deposit("ore_lapis", 2, .30, .82),
|
||||
new Deposit("ore_diamond_small", 2, .70, .94), new Deposit("ore_emerald", 1, .25, .85)
|
||||
};
|
||||
|
||||
public static void place(WorldGenLevel level, ExpansionChunkGenerator generator, ChunkAccess chunk, ExpansionIsland island) {
|
||||
if (island == null || generator.rootGeneration() != 24) return;
|
||||
int minX = chunk.getPos().getMinBlockX(), minZ = chunk.getPos().getMinBlockZ();
|
||||
// Density columns precede structures and vegetation. Cavities do not reset mineral depth:
|
||||
// retain the full top-to-bottom envelope rather than using the nearest cave ceiling.
|
||||
var column = generator.getBaseColumn(minX + 8, minZ + 8, level, generator.randomState(island));
|
||||
int top = -1, bottom = -1;
|
||||
for (int y = 1; y < 383; y++) if (column.getBlock(y).is(BlockTags.BASE_STONE_OVERWORLD)) {
|
||||
if (bottom < 0) bottom = y;
|
||||
top = y;
|
||||
}
|
||||
if (top - bottom < 24) return;
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.FEATURE);
|
||||
for (int index = 0; index < DEPOSITS.length; index++) {
|
||||
Deposit deposit = DEPOSITS[index];
|
||||
var feature = registry.getValue(Identifier.withDefaultNamespace(deposit.feature()));
|
||||
if (feature == null) throw new IllegalStateException("Missing native ore " + deposit.feature());
|
||||
var random = RandomSource.create(WoodlandGroveGeometry.seed(island.seed(), minX, index, minZ, 0x0280AE5L));
|
||||
for (int attempt = 0; attempt < deposit.attempts(); attempt++) {
|
||||
double depth = deposit.minDepth() + random.nextDouble() * (deposit.maxDepth() - deposit.minDepth());
|
||||
int y = (int) Math.round(top - depth * (top - bottom));
|
||||
// Largest selected native vein is 17 blocks (radius bound five). Its writes stay
|
||||
// in the decorating chunk, including where neighbours are generated in another order.
|
||||
var at = new BlockPos(minX + 5 + random.nextInt(6), y, minZ + 5 + random.nextInt(6));
|
||||
if (level.getBlockState(at).is(BlockTags.BASE_STONE_OVERWORLD)) feature.place(level, generator, random, at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
+24
@@ -17,6 +17,25 @@ 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<>());
|
||||
private static final Map<RandomState, PopulationHydrology.Plan> RIVERS30 = 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"));
|
||||
}
|
||||
public static PopulationHydrology.Plan river30(NoiseBasedChunkGenerator generator, RandomState state) {
|
||||
synchronized (RIVERS30) {
|
||||
return RIVERS30.computeIfAbsent(state, ignored -> {
|
||||
var sampler = state.getSampler(generator.generatorSettings().value().noiseRouter().finalDensity());
|
||||
var plan = River30.create(state.seed(), (int) capacity(generator).radius(),
|
||||
new SignMemo((x,y,z) -> sampler.sampleValue(SamplerContext.EMPTY_UNCACHED,x,y,z)));
|
||||
SanctuaryMod.LOGGER.info("River30 seed {}: {} cells, {} outlets, {} path points", state.seed(),
|
||||
plan.cells().size(), plan.spills().size(), plan.features().stream().mapToInt(f -> f.path().size()).sum());
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
}
|
||||
private PopulationHydrologyRuntime() {}
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
||||
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
||||
@@ -47,6 +66,10 @@ public final class PopulationHydrologyRuntime {
|
||||
});
|
||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||
if (current30(generator)) {
|
||||
water = River30.openBanks(seed, water, memo, 1)
|
||||
.overlay30(river30(generator, state).translated(-region.originX(), -region.originZ()));
|
||||
}
|
||||
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 +156,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());
|
||||
}
|
||||
|
||||
+83
-9
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -10,7 +11,9 @@ import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record PopulationIslandDensity(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity,
|
||||
DensityFunction caves28, DensityFunction ridges28, DensityFunction erosion28, boolean geology28,
|
||||
boolean terraces29, boolean relief30) 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),
|
||||
@@ -19,36 +22,106 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(PopulationIslandDensity::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(PopulationIslandDensity::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(PopulationIslandDensity::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity)
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(PopulationIslandDensity::capacity),
|
||||
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("terraces_29", false).forGetter(PopulationIslandDensity::terraces29),
|
||||
Codec.BOOL.optionalFieldOf("relief_30", false).forGetter(PopulationIslandDensity::relief30)
|
||||
).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, 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);
|
||||
}
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
var caves = geology28 ? caves28.compileSampler(context) : null;
|
||||
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 layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return 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;
|
||||
double sum = 0;
|
||||
// Trilinear sampling avoids integer snapping of the displaced native field.
|
||||
for (int dz = 0; dz <= (fz == 0 ? 0 : 1); dz++)
|
||||
for (int dy = 0; dy <= (fy == 0 ? 0 : 1); dy++)
|
||||
for (int dx = 0; dx <= (fx == 0 ? 0 : 1); dx++) {
|
||||
double w = (dx == 0 ? 1 - fx : fx) * (dy == 0 ? 1 - fy : fy)
|
||||
* (dz == 0 ? 1 - fz : fz);
|
||||
sum += w * raw.sampleValue(sample, ix + dx, iy + dy, iz + dz);
|
||||
}
|
||||
return (float) sum;
|
||||
}
|
||||
@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 natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
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);
|
||||
float coarseValue = coarse.sampleValue(sample, x, localY, z);
|
||||
float fineValue = fine.sampleValue(sample, x, localY, z);
|
||||
float terrainValue;
|
||||
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
|
||||
if (relief30) weight *= Relief30.influence(seed, x, z, capacity.radius());
|
||||
if (weight > 0) {
|
||||
var shift = 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));
|
||||
double sourceY = y - 64 - shift.y();
|
||||
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.
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
|
||||
edge.sampleValue(sample, x, 0, z), coarseValue,
|
||||
fineValue, bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
boolean affects = geology28 ? RiftShape.mayAffect28(layout, rx, rz) : RiftShape.mayAffect(layout, rx, rz);
|
||||
if (affects) natural = geology28
|
||||
? RiftShape.density28(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z))
|
||||
: RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
if (geology28 && natural > 0 && y > 40 && y < 232)
|
||||
natural = CavesRelief28.carve(natural, caves.sampleValue(sample, x, y - CavesRelief28.CAVE_OFFSET, z), y);
|
||||
return natural;
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
/** 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);
|
||||
}
|
||||
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;
|
||||
@@ -57,7 +130,8 @@ 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(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity,
|
||||
rule.rewrite(caves28), rule.rewrite(ridges28), rule.rewrite(erosion28), geology28, terraces29, relief30);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Three broad areas of signed deformation; the intervening rock keeps the original plateau field. */
|
||||
public final class Relief30 {
|
||||
private Relief30() {}
|
||||
public static double influence(long seed, int x, int z, double radius) {
|
||||
long mixed = seed ^ 0x30BADC0FFEEL;
|
||||
mixed = (mixed ^ (mixed >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
double rotation = (mixed >>> 11) * 0x1.0p-53 * Math.PI * 2;
|
||||
double strongest = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double angle = rotation + i * Math.PI * 2 / 3;
|
||||
double cx = Math.cos(angle) * radius * .52, cz = Math.sin(angle) * radius * .52;
|
||||
double dx = x - cx, dz = z - cz;
|
||||
double along = (dx * Math.cos(angle) + dz * Math.sin(angle)) / (radius * .47);
|
||||
double across = (-dx * Math.sin(angle) + dz * Math.cos(angle)) / (radius * .34);
|
||||
double distance = Math.hypot(along, across);
|
||||
double t = Math.clamp((1 - distance) / .70, 0, 1);
|
||||
strongest = Math.max(strongest, t * t * (3 - 2 * t));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,55 @@ public final class RiftShape {
|
||||
return (float) Math.max(-1.0, terrain - erosion);
|
||||
}
|
||||
|
||||
/** Alpha28 fractures lean sideways, end within the mass and retain interrupted rock spans. */
|
||||
public static boolean mayAffect28(Layout layout, int x, int z) {
|
||||
double radius = Math.hypot((double) x, z);
|
||||
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
if (near28(rift.main(), x, z)) return true;
|
||||
for (Segment branch : rift.branches()) if (near28(branch, x, z)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static float density28(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
|
||||
if (terrain <= 0 || y < 88 || y > 244 || !mayAffect28(layout, x, z)) return terrain;
|
||||
double coarse = Math.clamp(warped, -1, 1), fine = Math.clamp(detail, -1, 1), cut = 0;
|
||||
for (Rift rift : layout.rifts()) {
|
||||
cut = Math.max(cut, erosion28(rift.main(), rift.id(), x, y, z, coarse, fine));
|
||||
for (Segment branch : rift.branches())
|
||||
cut = Math.max(cut, erosion28(branch, rift.id(), x, y, z, coarse, fine));
|
||||
}
|
||||
double radius = Math.hypot((double) x, z);
|
||||
cut *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16)
|
||||
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20);
|
||||
return (float) Math.max(-1, terrain - cut);
|
||||
}
|
||||
|
||||
private static boolean near28(Segment segment, int x, int z) {
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 16
|
||||
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 20;
|
||||
}
|
||||
|
||||
private static double erosion28(Segment segment, long id, int x, int y, int z, double coarse, double fine) {
|
||||
if (!near28(segment, x, z)) return 0;
|
||||
double rx = x - segment.x(), rz = z - segment.z();
|
||||
double along = rx * segment.dx() + rz * segment.dz();
|
||||
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
|
||||
double centerY = 150 + unit(id + 41) * 32 + end * .12 + Math.sin(end / 29 + segment.phase()) * 7;
|
||||
double halfHeight = 22 + unit(id + 42) * 14;
|
||||
double vertical = smoothstep((halfHeight - Math.abs(y - centerY)) / 10);
|
||||
if (vertical <= 0) return 0;
|
||||
double lean = (y - centerY) * (.13 + unit(id + 43) * .07);
|
||||
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end) - lean - coarse * 1.05;
|
||||
double width = Math.clamp(segment.halfWidth() + fine * .5 + coarse * .45, 2.2, 5.7);
|
||||
double distance = Math.hypot(sideways, along - end);
|
||||
double bridgeAt = (unit(id + 44) - .5) * segment.halfLength();
|
||||
double bridge = smoothstep((Math.abs(end - bridgeAt) - 3) / 5);
|
||||
return 2.1 * smoothstep((width + 1.6 - distance) / 2.6) * vertical * bridge;
|
||||
}
|
||||
|
||||
private static boolean near(Segment segment, int x, int z) {
|
||||
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
|
||||
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrology.*;
|
||||
import java.util.*;
|
||||
|
||||
/** One island-scale route follows actual rock, with local water levels and short open banks. */
|
||||
public final class River30 {
|
||||
private static final int STEP = 8;
|
||||
private static final int[][] DIR = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||
private River30() {}
|
||||
private static long key(int x, int z) { return ((long)x << 32) ^ (z & 0xffffffffL); }
|
||||
private static int x(long k) { return (int)(k >> 32); }
|
||||
private static int z(long k) { return (int)k; }
|
||||
private record Node(long key, double cost) {}
|
||||
private static final class Rock {
|
||||
final Sampler sampler; final Map<Long,Integer> tops = new HashMap<>(); int samples;
|
||||
Rock(Sampler sampler) { this.sampler = sampler; }
|
||||
boolean solid(int x,int y,int z) { samples++; return y > 1 && y < 384 && sampler.sample(x,y,z) > 0; }
|
||||
int top(int x,int z) {
|
||||
return tops.computeIfAbsent(key(x,z), unused -> {
|
||||
for (int y=358;y>=140;y--) if (solid(x,y,z)) {
|
||||
for(int d=1;d<=6;d++) if(!solid(x,y-d,z)) return -1;
|
||||
return y;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
public static Plan create(long seed, int radius, Sampler sampler) {
|
||||
var rock = new Rock(sampler); var nodes = new TreeMap<Long,Integer>();
|
||||
int limit = radius / STEP * STEP;
|
||||
for(int x=-limit;x<=limit;x+=STEP) for(int z=-limit;z<=limit;z+=STEP) {
|
||||
if(Math.hypot(x,z)>radius*.95 || Math.hypot(x,z)<52) continue;
|
||||
int y=rock.top(x,z); if(y>=140) nodes.put(key(x,z),y);
|
||||
}
|
||||
// Ignore isolated peripheral rock when selecting opposite banks: route in the largest connected mass.
|
||||
var adjacency=new HashMap<Long,List<Long>>();
|
||||
for(long k:nodes.keySet()) {
|
||||
var neighbours=new ArrayList<Long>();
|
||||
for(var d:DIR) {
|
||||
long next=key(x(k)+d[0]*STEP,z(k)+d[1]*STEP);
|
||||
if(!nodes.containsKey(next)||Math.abs(nodes.get(next)-nodes.get(k))>48)continue;
|
||||
int gaps=0;for(int n=1;n<STEP;n++)if(rock.top(x(k)+d[0]*n,z(k)+d[1]*n)<140)gaps++;
|
||||
if(gaps<=2)neighbours.add(next);
|
||||
}
|
||||
adjacency.put(k,neighbours);
|
||||
}
|
||||
var remaining=new TreeSet<>(nodes.keySet());Set<Long> largest=Set.of();
|
||||
while(!remaining.isEmpty()) {
|
||||
var component=new HashSet<Long>();var queue=new ArrayDeque<Long>();queue.add(remaining.first());
|
||||
while(!queue.isEmpty()){long k=queue.remove();if(!component.add(k))continue;queue.addAll(adjacency.get(k));}
|
||||
remaining.removeAll(component);if(component.size()>largest.size())largest=component;
|
||||
}
|
||||
nodes.keySet().retainAll(largest);
|
||||
List<Long> best = List.of(); double longest = 0;
|
||||
// Four deterministic orientations avoid choosing an axis whose shore is entirely broken.
|
||||
for(int axis=0;axis<4;axis++) {
|
||||
double angle=(seed & 1023)*Math.PI/512 + axis*Math.PI/4, dx=Math.cos(angle), dz=Math.sin(angle);
|
||||
var ordered=nodes.keySet().stream().sorted(Comparator.<Long>comparingDouble(k->x(k)*dx+z(k)*dz).thenComparingLong(k->k)).toList();
|
||||
if(ordered.isEmpty()) break;
|
||||
long start=ordered.getFirst(); var costs=new HashMap<Long,Double>(); var parent=new HashMap<Long,Long>();
|
||||
var queue=new PriorityQueue<Node>(Comparator.comparingDouble(Node::cost).thenComparingLong(Node::key));
|
||||
costs.put(start,0.0);parent.put(start,start);queue.add(new Node(start,0));
|
||||
while(!queue.isEmpty()) {
|
||||
var at=queue.remove(); if(at.cost()!=costs.get(at.key())) continue;
|
||||
for(long next:adjacency.get(at.key())) {
|
||||
if(!nodes.containsKey(next)) continue;
|
||||
int difference=Math.abs(nodes.get(next)-nodes.get(at.key()));
|
||||
double cost=at.cost()+STEP+difference*difference*.18+Math.max(0,nodes.get(next)-215)*.13;
|
||||
if(cost>=costs.getOrDefault(next,Double.POSITIVE_INFINITY)) continue;
|
||||
costs.put(next,cost);parent.put(next,at.key());queue.add(new Node(next,cost));
|
||||
}
|
||||
}
|
||||
for(int i=ordered.size()-1;i>=0;i--) {
|
||||
long end=ordered.get(i); if(!parent.containsKey(end)) continue;
|
||||
double span=Math.hypot(x(end)-x(start),z(end)-z(start));
|
||||
if(span>longest) { var path=new ArrayList<Long>();for(long k=end;;k=parent.get(k)){path.add(k);if(k==start)break;}Collections.reverse(path);best=path;longest=span; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
var cells=new LinkedHashMap<Long,Cell>();var path=new ArrayList<Point>(); long id=0x300000000L;
|
||||
if(longest>=radius*.7) for(int i=1;i<best.size();i++) {
|
||||
long a=best.get(i-1),b=best.get(i);int dx=Integer.signum(x(b)-x(a)),dz=Integer.signum(z(b)-z(a));
|
||||
for(int n=0;n<=STEP;n++) {
|
||||
int cx=x(a)+dx*n,cz=z(a)+dz*n;path.add(new Point(cx,cz));
|
||||
int center=rock.top(cx,cz);
|
||||
for(int side=-2;side<=2;side++) {
|
||||
int px=cx+dz*side,pz=cz-dx*side,top=rock.top(px,pz);
|
||||
if(top<140 || Math.abs(top-center)>5) continue;
|
||||
// The bed stays in existing rock. Higher neighbouring faces become natural banks.
|
||||
int level=Math.min(top-1,center-1),bed=level-2;boolean support=true;
|
||||
for(int y=bed-3;y<=bed;y++) if(!rock.solid(px,y,pz)) {support=false;break;}
|
||||
if(support) cells.put(key(px,pz),new Cell(px,pz,level,bed,top,side==0?ShoreMaterial.GRAVEL:ShoreMaterial.CLAY,id,2));
|
||||
}
|
||||
}
|
||||
}
|
||||
System.getLogger(River30.class.getName()).log(System.Logger.Level.INFO,"River30 connectedNodes="+nodes.size()+" span="+longest+" columns="+rock.tops.size());
|
||||
var features=new ArrayList<Feature>();
|
||||
if(!cells.isEmpty()) features.add(feature(id,Kind.RIVER,cells.values(),path));
|
||||
var plan=PopulationHydrology.additional30(cells,features,List.of(),rock.tops.size(),rock.samples);
|
||||
return openBanks(seed,plan,sampler,6);
|
||||
}
|
||||
/** 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 candidates=water.cells().stream().filter(Cell::hasWater).sorted(Comparator
|
||||
.<Cell>comparingLong(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) {
|
||||
if(water.cellAt(wet.x()+d[0],wet.z()+d[1])!=null) 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,wet.waterY()+2,ShoreMaterial.STONE,id,0));
|
||||
}
|
||||
if(conflict) 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(),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);}
|
||||
}
|
||||
+22
-4
@@ -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;
|
||||
@@ -183,7 +190,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
if (generation == 24) for (var site : aerialPlan24.sites()) {
|
||||
if (minX <= site.maxX() + 1 && maxX >= site.minX() - 1 && minZ <= site.maxZ() + 1 && maxZ >= site.minZ() - 1
|
||||
&& minY <= site.maxY() + 1 && maxY >= (site.waterfall() ? 0 : site.minY() - 1)) return true;
|
||||
&& minY <= site.maxY() + 1 && maxY >= site.minY() - 1) return true;
|
||||
if (site.waterfall()) {
|
||||
var flow = site.waterfallClaimBounds();
|
||||
if (minX <= flow.maxX() && maxX >= flow.minX() && minZ <= flow.maxZ() && maxZ >= flow.minZ()
|
||||
&& minY <= flow.maxY() && maxY >= flow.minY()) return true;
|
||||
}
|
||||
}
|
||||
return (generation == 23 || generation == 24) && mineralSanctuary23.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
|| (generation == 17 || generation == 19 || generation == 20 || generation == 21 || generation == 22 || (generation == 23 || generation == 24)) && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
@@ -210,6 +222,10 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
case "sanctuary:population_rocky_heath" -> "meadow";
|
||||
case "sanctuary:population_dark_grove" -> "dark_forest";
|
||||
case "sanctuary:population_bamboo_grove" -> "bamboo_jungle";
|
||||
case "sanctuary:swamp27_caves" -> "swamp";
|
||||
case "sanctuary:population_lush_caves" -> "lush_caves";
|
||||
case "sanctuary:population_dripstone_caves" -> "dripstone_caves";
|
||||
case "sanctuary:population_sulfur_depths" -> "sulfur_caves";
|
||||
default -> null;
|
||||
};
|
||||
return equivalent == null ? biome : registries.lookupOrThrow(Registries.BIOME).getOrThrow(
|
||||
@@ -335,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()));
|
||||
@@ -388,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(); }
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class Swamp27Volumes {
|
||||
// Broad, connected sectors change with height; finer noise softens their borders.
|
||||
double broad = noise(seed, x / 94.0, y / 48.0, z / 94.0);
|
||||
double detail = noise(seed ^ 0x7757A27L, x / 37.0, y / 31.0, z / 37.0);
|
||||
return broad + detail * .22 > .04 + (1 - verticalEdge) * .7;
|
||||
return broad + detail * .22 > .25 + (1 - verticalEdge) * .7;
|
||||
}
|
||||
|
||||
private static double noise(long seed, double x, double y, double z) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+116
-16
@@ -15,11 +15,15 @@ import fr.koka.sanctuary.worldgen.aerial.AerialPlan24.Site;
|
||||
public final class AerialPlanner24 {
|
||||
public static final int MAX_ATTEMPTS_PER_SITE = 64;
|
||||
public static final int MAX_COLUMNS = 8192;
|
||||
public static final int ARRIVAL_RADIUS = 96;
|
||||
public static final int MIN_ISLET_GAP = 16;
|
||||
private AerialPlanner24() {}
|
||||
public interface Terrain {
|
||||
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) {
|
||||
@@ -28,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);
|
||||
}
|
||||
@@ -56,26 +63,68 @@ public final class AerialPlanner24 {
|
||||
// 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;
|
||||
Shore shore = shore(probe, angle, radius);
|
||||
if (shore == null) continue;
|
||||
int facing = (int) Math.floorMod(mix(draw), 4);
|
||||
int worldWidth = facing % 2 == 0 ? width : depth, worldDepth = facing % 2 == 0 ? depth : width;
|
||||
// The full claim remains outside initial terrain ownership, including corner chunks.
|
||||
double distance = radius + 32 + Math.hypot(worldWidth, worldDepth) / 2 + 64 + (attempt % 4) * 20;
|
||||
int cx = (int) Math.round(Math.cos(angle) * distance), cz = (int) Math.round(Math.sin(angle) * distance);
|
||||
if (Math.hypot(cx - shore.x, cz - shore.z) > 500) continue;
|
||||
int bottom = merchant ? Math.clamp(shore.y - 4 + (int) Math.floorMod(draw, 13), 180, 300)
|
||||
: Math.clamp(shore.y + 20 - bodyHeight / 2, 210, 320 - bodyHeight);
|
||||
bottom = Math.min(bottom, 350 - height);
|
||||
Shore shore;
|
||||
int cx, cz, bottom;
|
||||
if (merchant) {
|
||||
shore = shore(probe, angle, radius);
|
||||
if (shore == null) continue;
|
||||
// Start from the real edge, not the nominal island radius. The projected half-box
|
||||
// puts the complete hull 16–32 blocks out, even when the ship is rotated.
|
||||
double dx = Math.cos(angle), dz = Math.sin(angle);
|
||||
double offset = Math.abs(dx) * worldWidth / 2.0 + Math.abs(dz) * worldDepth / 2.0
|
||||
+ 16 + (attempt % 3) * 8;
|
||||
cx = shore.x + (int) Math.round(dx * offset);
|
||||
cz = shore.z + (int) Math.round(dz * offset);
|
||||
// Native boarding deck is five blocks above the envelope's bottom.
|
||||
bottom = shore.y - 5 + (int) Math.floorMod(draw, 5);
|
||||
} else {
|
||||
// Both mineral islands hover over Sanctuary, away from its natural arrival.
|
||||
double distance = radius * (.48 + .15 * unit(mix(draw ^ 0x28A1L)));
|
||||
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 = 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)
|
||||
highest = Math.max(highest, probe.highest(cx + ox, cz + oz));
|
||||
bottom = highest + MIN_ISLET_GAP + 1;
|
||||
}
|
||||
if (bottom < 32 || bottom + height > 350) continue;
|
||||
var site = new Site(ids[ordinal], kinds[ordinal], cx - worldWidth / 2, bottom, cz - worldDepth / 2,
|
||||
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2);
|
||||
if (site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
|
||||
boolean rootOverlap = site.claimedChunks().stream().anyMatch(c -> {
|
||||
int nearestX = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
|
||||
int nearestZ = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
|
||||
return Math.hypot(nearestX, nearestZ) <= radius + 32;
|
||||
});
|
||||
if (rootOverlap || !visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
|
||||
if (!outsideArrival(site) || site.claimedChunks().stream().anyMatch(c -> claimed.contains(c.pack()))) continue;
|
||||
if (merchant) {
|
||||
double gap = shoreDistance(site);
|
||||
if (gap < 16 || gap > 48 || !clearShip(probe, site)) continue;
|
||||
} 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.
|
||||
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 (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));
|
||||
}
|
||||
bottom = Math.max(bottom, highest + MIN_ISLET_GAP + 1);
|
||||
if (bottom + height > 350) continue;
|
||||
site = new Site(site.id(), site.kind(), site.minX(), bottom, site.minZ(), width, height, depth,
|
||||
facing, siteSeed, shore.x, shore.y, shore.z, site.waterfall());
|
||||
if (!clearReservations(probe, site)) continue;
|
||||
}
|
||||
if (!visible(probe, shore, cx, bottom + bodyHeight - 4, cz)) continue;
|
||||
accepted = site; break;
|
||||
}
|
||||
if (accepted == null) throw new IllegalStateException("aerial_sites_unavailable: could not place " + ids[ordinal] + " within the bounded search");
|
||||
@@ -89,6 +138,39 @@ public final class AerialPlanner24 {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Horizontal distance from the saved land witness to the nearest point of the hull envelope. */
|
||||
public static double shoreDistance(Site site) {
|
||||
return Math.hypot(site.shoreX() - Math.clamp(site.shoreX(), site.minX(), site.maxX()),
|
||||
site.shoreZ() - Math.clamp(site.shoreZ(), site.minZ(), site.maxZ()));
|
||||
}
|
||||
private static boolean outsideArrival(Site site) {
|
||||
if (Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) < ARRIVAL_RADIUS) return false;
|
||||
if (site.waterfall()) {
|
||||
var box = site.waterfallClaimBounds();
|
||||
if (Math.hypot(Math.clamp(0, box.minX(), box.maxX()), Math.clamp(0, box.minZ(), box.maxZ())) < ARRIVAL_RADIUS) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static boolean isletColumn(Site site, int x, int z) {
|
||||
return AerialGeometry24.radial(site, x, z) <= 1
|
||||
|| site.waterfall() && Math.abs(x - site.width() / 2) <= 2 && z <= 6;
|
||||
}
|
||||
private static boolean clearShip(Probe probe, Site site) {
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
|
||||
if (probe.highest(x, z) >= site.minY() - 3 || probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
|
||||
return true;
|
||||
}
|
||||
private static boolean clearReservations(Probe probe, Site site) {
|
||||
for (int x = site.minX(); x <= site.maxX(); x++) for (int z = site.minZ(); z <= site.maxZ(); z++)
|
||||
if (probe.terrain.reserved(x, z, site.minY() - 1, site.maxY() + 1)) return false;
|
||||
if (site.waterfall()) {
|
||||
var box = site.waterfallClaimBounds();
|
||||
for (int x = box.minX(); x <= box.maxX(); x++) for (int z = box.minZ(); z <= box.maxZ(); z++)
|
||||
if (probe.terrain.reserved(x, z, box.minY(), box.maxY())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Shore shore(Probe probe, double angle, int radius) {
|
||||
double dx = Math.cos(angle), dz = Math.sin(angle);
|
||||
// An outside-in scan finds the outer shore, never an internal pond. Refine its edge to a block.
|
||||
@@ -120,6 +202,19 @@ 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;
|
||||
final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
@@ -138,6 +233,11 @@ public final class AerialPlanner24 {
|
||||
for (int y = 382; y > 0; y--) if (column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty()) return y;
|
||||
return 0;
|
||||
}
|
||||
int highest(int x, int z) {
|
||||
var column = column(x, z); if (column == null) return 0;
|
||||
for (int y = 382; y > 0; y--) if (!column.getBlock(y).isAir()) return y;
|
||||
return 0;
|
||||
}
|
||||
boolean solid(int x, int y, int z) {
|
||||
var column = column(x, z); return column != null && !column.getBlock(y).isAir();
|
||||
}
|
||||
|
||||
@@ -95,13 +95,9 @@ public final class AerialStore24 {
|
||||
|
||||
private static void validateExtent(AerialPlan24 plan, int diameter) throws IOException {
|
||||
if (diameter != 512 && diameter != 724 && diameter != 1024) throw new IOException("Unsupported aerial root size");
|
||||
int radius = (diameter == 724 ? 395 : diameter / 2 + 32) + 32;
|
||||
for (long packed : plan.claimsChunks()) {
|
||||
var c = net.minecraft.world.level.ChunkPos.unpack(packed);
|
||||
int x = Math.clamp(0, c.getMinBlockX(), c.getMaxBlockX());
|
||||
int z = Math.clamp(0, c.getMinBlockZ(), c.getMaxBlockZ());
|
||||
if (Math.hypot(x, z) <= radius) throw new IOException("Aerial claim overlaps initial terrain ownership");
|
||||
}
|
||||
// Aerial bodies may share X/Z with the initial island. Their vertical clearance is
|
||||
// certified at creation; the immutable manifest preserves it on subsequent loads.
|
||||
// Site/Plan constructors still validate finite height, dimensions, count and disjoint claims.
|
||||
}
|
||||
private static JsonObject encode(AerialPlan24 plan, long seed, int diameter) {
|
||||
var root = new JsonObject();
|
||||
|
||||
+5
-2
@@ -22,7 +22,7 @@ import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Native starts for four immutable initial sites. Their void chunks need no terrain island owner. */
|
||||
/** Native starts for four immutable initial sites, in the void or above initial terrain. */
|
||||
public final class AerialStructures24 {
|
||||
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
|
||||
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
|
||||
@@ -67,7 +67,10 @@ public final class AerialStructures24 {
|
||||
if (previous != null && previous.isValid()) continue;
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, origin.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
// Only this explicit saved departure bypasses biome selection. Its complete
|
||||
// geometry and clearance were certified by the aerial planner; the biome below
|
||||
// may be forest, marsh or void. findGenerationPoint still requires this exact plan.
|
||||
chunk, ignoredBiome -> sites.stream().anyMatch(site -> key(site).equals(key)));
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -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; }
|
||||
|
||||
+194
@@ -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; }
|
||||
}
|
||||
+2
-1
@@ -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; }
|
||||
|
||||
+3
-1
@@ -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. */
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:max",
|
||||
"left": {
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:min",
|
||||
"left": {
|
||||
"type": "minecraft:add",
|
||||
"left": {
|
||||
"type": "minecraft:mul",
|
||||
"left": {
|
||||
"type": "minecraft:square",
|
||||
"input": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:cave_layer",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 8.0
|
||||
}
|
||||
},
|
||||
"right": 4.0
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:clamp",
|
||||
"input": {
|
||||
"type": "minecraft:add",
|
||||
"left": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:cave_cheese",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.6666666666666666
|
||||
},
|
||||
"right": 0.27
|
||||
},
|
||||
"max": 1.0,
|
||||
"min": -1.0
|
||||
}
|
||||
},
|
||||
"right": "minecraft:overworld/caves/entrances"
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:add",
|
||||
"left": "minecraft:overworld/caves/spaghetti_2d",
|
||||
"right": "minecraft:overworld/caves/spaghetti_roughness_function"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"type": "minecraft:range_choice",
|
||||
"input": "minecraft:overworld/caves/pillars",
|
||||
"max_exclusive": 0.03,
|
||||
"min_inclusive": -1000000.0,
|
||||
"when_in_range": -1000000.0,
|
||||
"when_out_of_range": "minecraft:overworld/caves/pillars"
|
||||
}
|
||||
},
|
||||
"right": "minecraft:overworld/caves/noodle"
|
||||
}
|
||||
+17
-1
@@ -49,5 +49,21 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
+17
-1
@@ -49,5 +49,21 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
+17
-1
@@ -49,5 +49,21 @@
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
},
|
||||
"geology_28": true,
|
||||
"caves_28": "sanctuary:caves28_native",
|
||||
"ridges_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:ridge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"erosion_28": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "minecraft:erosion",
|
||||
"xz_scale": 2.0,
|
||||
"y_scale": 2.0
|
||||
},
|
||||
"terraces_29": false,
|
||||
"relief_30": true
|
||||
}
|
||||
|
||||
+83
@@ -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,54 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
/** Adapters and finite fractures only. Native noise presence is checked separately in Minecraft. */
|
||||
public final class CavesRelief28Smoke {
|
||||
public static void main(String[] args) {
|
||||
for (int y = 0; y < 384; y++) {
|
||||
require(CavesRelief28.deformationWeight(0, y, 0, 256) == 0, "arrival must not deform");
|
||||
require(CavesRelief28.deformationWeight(256, y, 0, 256) == 0, "outer coast must not deform");
|
||||
if (y <= 180) require(CavesRelief28.deformationWeight(128, y, 0, 256) == 0,
|
||||
"accepted lower mass must not deform");
|
||||
}
|
||||
require(CavesRelief28.deformationWeight(128, 220, 0, 256)
|
||||
< CavesRelief28.deformationWeight(128, 228, 0, 256), "3D deformation must have a smooth vertical envelope");
|
||||
for (float v : new float[] {-10, -1, -.5F, 0, .5F, 1, 10}) {
|
||||
var warp = CavesRelief28.warp(1, v, v, v);
|
||||
var opposite = CavesRelief28.warp(1, -v, -v, -v);
|
||||
require(Math.abs(warp.x()) <= 16 && Math.abs(warp.z()) <= 16 && Math.abs(warp.y()) <= 40,
|
||||
"deformation must stay bounded in all three axes");
|
||||
require(warp.x() == -opposite.x() && warp.y() == -opposite.y() && warp.z() == -opposite.z(),
|
||||
"noise must permit equivalent uplift and subsidence, without positive bias");
|
||||
var fixed = CavesRelief28.warp(0, v, v, v);
|
||||
require(fixed.x() == 0 && fixed.y() == 0 && fixed.z() == 0, "zero envelope leaves coordinates unchanged");
|
||||
}
|
||||
require(CavesRelief28.warp(1, 0, .5F, 0).y() < CavesRelief28.warp(1, 0, .6F, 0).y(),
|
||||
"deformation must not saturate into broad flat plateaus");
|
||||
for (int y = 0; y < 384; y++) for (float cave : new float[] {-1, -.1F, .1F, 1, 64}) {
|
||||
require(CavesRelief28.carve(-.3F, cave, y) == -.3F, "caves must never fill existing void");
|
||||
require(CavesRelief28.carve(.7F, cave, y) <= .7F, "caves must only remove rock");
|
||||
if (y <= 40 || y >= 232) require(CavesRelief28.carve(.7F, cave, y) == .7F, "bounded cave heights");
|
||||
}
|
||||
require(CavesRelief28.carve(.7F, -.1F, 140) < 0, "negative native cave must open solid interior");
|
||||
for (long seed : new long[] {0, 42, 2026}) {
|
||||
var layout = RiftShape.layout(seed);
|
||||
int removed = 0, retained = 0;
|
||||
for (int x = -216; x <= 216; x += 4) for (int z = -216; z <= 216; z += 4) {
|
||||
require(RiftShape.density28(layout, x, 60, z, .7F, 0, 0) == .7F, "rift cannot cut lower boundary");
|
||||
require(RiftShape.density28(layout, x, 260, z, .7F, 0, 0) == .7F, "rift cannot traverse upper surface");
|
||||
int columnCut = 0;
|
||||
for (int y = 88; y <= 244; y += 4) {
|
||||
float value = RiftShape.density28(layout, x, y, z, .7F, .2F, -.3F);
|
||||
require(value <= .7F, "rift cannot add material");
|
||||
if (value < 0) { removed++; columnCut++; } else retained++;
|
||||
}
|
||||
require(columnCut < 22, "fracture cannot cut the entire vertical island mass");
|
||||
}
|
||||
require(removed > 0 && retained > removed, "finite ravines must open passages and retain rock");
|
||||
System.out.println("caves28 rifts seed=" + seed + " removed=" + removed + " retained=" + retained);
|
||||
}
|
||||
System.out.println("CavesRelief28Smoke: signed 3D deformation, native cut adapter and bounded ravines passed");
|
||||
}
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
public final class River30Smoke {
|
||||
public static void main(String[] args) {
|
||||
for (int radius : new int[]{256,362,512}) {
|
||||
PopulationHydrology.Sampler rock=(x,y,z)->Math.hypot(x,z)<radius*.82 && y>=60 && y<=230 ? 1 : -1;
|
||||
var plan=River30.create(42,radius,rock);
|
||||
var river=plan.features().stream().filter(f->f.kind()==PopulationHydrology.Kind.RIVER).findFirst().orElseThrow();
|
||||
require(river.bounds().maxX()-river.bounds().minX()>radius || river.bounds().maxZ()-river.bounds().minZ()>radius,"Island-wide route");
|
||||
require(plan.spills().size()>0,"A nearby edge can discharge into native void");
|
||||
for(var c:plan.cells()) {
|
||||
if(plan.isOutletCell(c)) {require(c.sedimentDepth()==0&&!c.hasWater(),"Outlet only removes its bank");continue;}
|
||||
require(Math.hypot(c.x(),c.z())>=49,"Arrival remains natural");
|
||||
for(int y=c.bedY()-c.sedimentDepth()-1;y<=c.bedY();y++) require(rock.sample(c.x(),y,c.z())>0,"Bed consists of native rock");
|
||||
}
|
||||
for(var spill:plan.spills()) require(Math.abs(spill.outlet().x()-spill.source().x())+Math.abs(spill.outlet().z()-spill.source().z())<=5,"Bank cut at most five blocks");
|
||||
require(plan.cells().equals(River30.create(42,radius,rock).cells()),"Immutable deterministic plan");
|
||||
}
|
||||
for(long seed:new long[]{0,42,2026,-7228211907433324401L}) {
|
||||
int untouched=0;
|
||||
for(int x=-350;x<=350;x+=10)for(int z=-350;z<=350;z+=10)if(Math.hypot(x,z)<350 && Relief30.influence(seed,x,z,362)==0)untouched++;
|
||||
require(untouched>1000,"Wide undeformed regions remain between the three deformation areas");
|
||||
}
|
||||
System.out.println("River30: island-scale supported channels, five-block open banks, natural arrival, deterministic plans and separated relief regions passed.");
|
||||
}
|
||||
static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -48,12 +48,24 @@ public final class Aerial24Smoke {
|
||||
&& site.height() == site.rockHeight() + AerialPlan24.VEGETATION_HEADROOM,
|
||||
"Vegetation adds eight blocks of headroom without enlarging the stone body");
|
||||
double distance = Math.hypot(site.locator().getX() - site.shoreX(), site.locator().getZ() - site.shoreZ());
|
||||
require(distance <= 512, "Every site has a nearby shore witness");
|
||||
require(distance <= 512, "Every site has a nearby ground witness");
|
||||
if (site.merchant()) {
|
||||
double gap = AerialPlanner24.shoreDistance(site);
|
||||
require(gap >= 16 && gap <= 48, "Complete ship envelope is 16–48 blocks from its actual shore");
|
||||
require(Math.abs(AerialGeometry24.entry(site).getY() - site.shoreY()) <= 4,
|
||||
"Boarding deck remains near the shore's walking height");
|
||||
} else {
|
||||
require(root.ownsChunk(site.startChunkX(), site.startChunkZ()), "Mineral islets hover over the initial island");
|
||||
require(site.minY() - 240 - 1 >= AerialPlanner24.MIN_ISLET_GAP,
|
||||
"The unchanged rocky body has at least sixteen air blocks above the terrain");
|
||||
}
|
||||
require(Math.hypot(Math.clamp(0, site.minX(), site.maxX()), Math.clamp(0, site.minZ(), site.maxZ())) >= AerialPlanner24.ARRIVAL_RADIUS,
|
||||
"Aerial bodies preserve the natural arrival zone");
|
||||
require(plan.atStartChunk(site.startChunkX(), site.startChunkZ()).contains(site), "Native departure belongs to its saved site");
|
||||
for (var chunk : site.claimedChunks()) {
|
||||
require(allClaims.add(chunk.pack()), "Site claims do not overlap");
|
||||
require(site.ownsChunk(chunk.x(), chunk.z()) && plan.ownsChunk(chunk.x(), chunk.z()), "Claim query agrees with the immutable mask");
|
||||
require(!root.ownsChunk(chunk.x(), chunk.z()), "Claim does not replace initial island ownership");
|
||||
|
||||
}
|
||||
var overlapping = new ExpansionIsland("overlap", "sanctuary", "east", "temperate", 64,
|
||||
site.startChunkX() * 16, site.startChunkZ() * 16, 42, "natural", "reserved", 24);
|
||||
@@ -109,7 +121,7 @@ public final class Aerial24Smoke {
|
||||
require(columns[0] <= AerialPlanner24.MAX_COLUMNS, "Reservations preserve the bounded terrain sampling budget");
|
||||
for (var site : replacement.sites())
|
||||
require(!terrain.reserved(site.shoreX(), site.shoreZ(), site.shoreY() - 1, site.shoreY() + 2),
|
||||
"Every accepted shore stays outside planned root structures");
|
||||
"Every accepted ground witness stays outside planned root structures");
|
||||
}
|
||||
}
|
||||
private static AerialPlan24.Site rotate(AerialPlan24.Site site, int facing) {
|
||||
|
||||
+5
-2
@@ -51,6 +51,9 @@ public final class AerialPersistence24Smoke {
|
||||
require(draws.get() == 1 && stored.plan().sites().size() == 4, "One complete initial plan is recorded");
|
||||
var journal = new ExpansionJournal(directory, SEED, 724, 24, stored.token());
|
||||
var root = ExpansionIsland.origin(724, SEED, 24);
|
||||
require(stored.plan().sites().stream().filter(site -> site.kind() == Kind.MINERAL_ISLET)
|
||||
.allMatch(site -> root.ownsChunk(site.startChunkX(), site.startChunkZ())),
|
||||
"An immutable elevated islet may share initial terrain chunks without replacing root ownership");
|
||||
Path journalFile = directory.resolve(ExpansionJournal.FILE_NAME);
|
||||
byte[] pristineJournal = Files.readAllBytes(journalFile);
|
||||
require(JsonParser.parseString(Files.readString(journalFile)).getAsJsonObject().get("schema").getAsInt() == 2,
|
||||
@@ -143,8 +146,8 @@ public final class AerialPersistence24Smoke {
|
||||
return new AerialPlan24(List.of(
|
||||
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 28, 0, 2, 0, 240, 390, false),
|
||||
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 28, 0, 3, -390, 240, 0, false),
|
||||
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -450, 255, -650, 48, 36, 48, 0, 4, -200, 240, -330, true),
|
||||
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 460, 260, -600, 56, 40, 56, 0, 5, 250, 240, -300, false)));
|
||||
new Site("mineral_islet_water", Kind.MINERAL_ISLET, -180, 270, -160, 48, 36, 48, 0, 4, -156, 240, -136, true),
|
||||
new Site("mineral_islet_dry", Kind.MINERAL_ISLET, 125, 270, -170, 56, 40, 56, 0, 5, 153, 240, -142, false)));
|
||||
}
|
||||
private static String digest(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
|
||||
private static void require(boolean value, String message) { if (!value) 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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.27"
|
||||
version = "0.1.0-alpha.30"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user