Compare commits

...
Author SHA1 Message Date
koka 79d3185cba feat(worldgen): add volumetric caves and nearby aerial sites for alpha28
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 01:36:05 +02:00
koka ad234a808f docs: record verified alpha27 publication and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 00:37:49 +02:00
koka 392ade0b7e Release alpha27 from alpha24 terrain with cave swamps and restored vanilla ships
Build Sanctuary / build (push) Canceled after 0s
2026-09-12 00:36:17 +02:00
61 changed files with 2401 additions and 373 deletions
+1
View File
@@ -15,3 +15,4 @@ screenshots/
.env
.env.*
!.env.example
__pycache__/
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## 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 lalpha.25. Le relief et les
souterrains de la 24 sont conservés ; la montgolfière est retirée. Première
passe limitée aux marais souterrains et à des épaves vanilla redressées
et restaurées pour les navires marchands. Les nouvelles
montagnes, cavités et garanties océaniques propres à la 25 ne sont pas reprises.
Alpha.26 sautée ; essai sur nouveau monde, sans migration. Voir le
[contrat alpha.27](docs/generation-alpha27.md) et les [vérifications](docs/testing-alpha27.md).
## 0.1.0-alpha.20 — 2026-09-10
- Passe architecturale sur les six bâtiments urbains, les quatorze anciennes
+38 -5
View File
@@ -15,9 +15,42 @@ infrastructures qui permettront progressivement d'ouvrir de nouveaux continents.
Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md)
conserve les intentions ; le [backlog](docs/backlog.md) suit les
tickets. Les systèmes d’économie, de progression collective et les dimensions
décrits dans la vision ne sont pas encore implémentés. **Lalpha.24 est
publiée** : cinq lieux suspendus accompagnent l’île naturelle et ses secrets.
Voir [les essais](docs/testing-alpha24.md) et [la distribution](docs/packwiz.md).
décrits dans la vision ne sont pas encore implémentés. **Lalpha.27 repart de
lalpha.24**, après le rejet du rendu de lalpha.25. Cette première passe conserve
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.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
et couches de planification de lalpha.25. Les changements de relief seront
repris séparément après un essai visuel. Lalpha.26 est sautée à la demande
du créateur.
Les deux navires et les deux îlots restent présents, avec un seul îlot à
cascade. Les marais occupent des volumes de biomes dans les cavités existantes,
sans produire une nouvelle géométrie de grotte. Ce retour ne prétend pas
remplacer le creusement Sanctuary par lensemble du moteur de grottes vanilla.
Créer un **nouveau monde Sanctuary** ; tailles 512 / 724 / 1 024 et Moyen par
défaut conservés. Le codec de terrain reste `sanctuary:island_v24` dans ce
prototype alpha.27 ; la reprise des anciennes sauvegardes nest pas un objectif.
La distribution et les vérifications effectuées sont détaillées dans
[les essais alpha.27](docs/testing-alpha27.md) et [packwiz](docs/packwiz.md).
## Alpha.24 — les lieux suspendus
@@ -552,7 +585,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.28 |
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
@@ -581,7 +614,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.28.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).
+24
View File
@@ -954,3 +954,27 @@ herbes et fleurs. [Validation](testing-alpha24.md) réussie sur Mac : 14 scénar
Release alpha.24 publiée ; canal packwiz et instance Prism existante
synchronisés deux fois. Les 925 fichiers personnels et réglages suivis sont
conservés. Aucun essai Windows natif revendiqué.
### WG-29 — Reprendre la génération par petits incréments — alpha.27
Branche `codex/reprise-sanctuary-alpha27`, directement depuis `e7d1f29` (alpha.24).
La 25 est rejetée pour son rendu artificiel : les validations fonctionnelles ne
validaient pas lexpérience visuelle attendue. Alpha.26 sautée.
Première passe : retrouver le terrain 24, enlever la montgolfière, proposer des
marais volumétriques dans les cavités existantes et affiner les coques. Voir le
[contrat](generation-alpha27.md). Les étagements, l’érosion, les nouvelles grottes
et les petites sorties deau 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 lalpha.25.
Résultats et limites : [génération](generation-alpha28.md), [essais](testing-alpha28.md).
+64
View File
@@ -0,0 +1,64 @@
# Alpha.27 — reprendre la 24 et regarder le résultat
Le retour du 12 septembre 2026 rejette l'alpha.25 : cônes creux, enveloppes
rocheuses trop minces, cavités génériques et île trop perforée. Les tests
fonctionnels passaient mais ne démontraient pas un rendu naturel. La branche
`codex/reprise-sanctuary-alpha27` repart directement de `e7d1f29`, la base 24
publiée ; elle ne désactive pas simplement quelques options de la 25.
## Cette première passe
- Retrouver le relief, les cavités et l'hydrologie de la 24. Aucun ajout de
montagne, volume de cavité, lac ou plateforme pour faire tenir un biome.
- Retirer la montgolfière, son placement et ses réservations ; conserver les
deux navires marchands et les deux îlots minéraux, dont un seul à cascade.
- Reprendre directement `minecraft:shipwreck/rightsideup_full`, une épave
vanilla complète déjà à lendroit, puis restaurer les trous, les mâts et
laccès. Conserver ses blocs, dalles, escaliers et sa cabine, au lieu de
dessiner une nouvelle coque approximative. Garder les marchands et un
coffre de cargaison par navire.
- Répartir des secteurs de marais en trois dimensions dans les souterrains
existants, remplaçant localement les autres biomes de grottes. Le sol et les
végétaux changent, pas la densité rocheuse. Chêne et végétation humide plutôt
que bosquets de chêne noir dans ces secteurs.
Minecraft reste en **26.3-pre-2**. Les tailles restent **512 / 724 / 1 024**, Moyen
par défaut, avec le bouton natif Personnaliser. Les options existantes des
structures gardent leur rôle. Les marais sont naturels, indépendants de ces
options. Le Deep Dark n'est pas ajouté.
Le prototype alpha.27 conserve le codec `sanctuary:island_v24` et ses ressources
de terrain. Le créateur teste des mondes jetables et a levé l'objectif de
compatibilité ; on ne convertit ni ne régénère ses sauvegardes. **Nouveau monde
nécessaire.** Les archives de la 25 restent sur `codex/terrain-sanctuary-alpha25`,
commit de livraison `df51e17` et documentation `1bf3a3b`.
## Ce qui reste à travailler après l'essai
Les plateaux étagés, l'érosion, les galeries proches des bruits de grottes
vanilla et les sorties d'eau libres ne sont pas réintroduits en bloc. Ils
seront examinés séparément, à partir de ce que l'on voit en jeu. Des points
d'eau peuvent retomber sur l'île ou dans le vide ; leur intérêt ne dépend pas
d'une arrivée dans un bassin calculé.
Mangroves, grenouilles, slimes et lucioles restent les références de l'écologie
des marais. La liste réellement présente et les restrictions natives sont
consignées dans [les vérifications](testing-alpha27.md), sans promettre une
population observée à partir d'une simple entrée dans un biome.
Les fonctions propres à la 25 — nouveau village souterrain, cabane du mineur,
cadre du sanctuaire et garantie spécifique du monument — sont écartées avec
sa base. La 24 conserve ses structures et expansions ; leur présence n'est
pas présentée comme une reprise de tous les contrats de la 25.
## Méthode retenue
Une passe limitée, un pack jouable, puis un retour sur son apparence. Vérifier
le build et les nouveaux points de comportement ne remplace pas ce retour.
Pas de matrice exhaustive ni de benchmark supplémentaire avant cet essai.
Les prochaines optimisations devront conserver les formes jugées bonnes,
au lieu de fabriquer des espaces faciles à certifier.
Le benchmark archivé de la 25 mesurait 169,816 s contre 60,932 s pour un parcours
de 369 chunks (générations 24/25 dans les mêmes sources 25). Il documente un coût
sur ce témoin Mac ; il ne sauve pas le rendu refusé et ne mesure pas cette 27.
+61
View File
@@ -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 satténuent vers les limites verticales ; il ne
sagit 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. Lenveloppe de l’île reste
évaluée aux coordonnées réelles ; seul le champ rocheux interne est déplacé.
Ce nest pas une simulation physique de plaques tectoniques. Aucun cône ni
salle géométrique ne sert de forme à ajouter au terrain.
Larrivé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 sappliquent 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 deau. Leurs corps et leurs chutes deau 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 lensemble 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 nest 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 lenveloppe 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 nest 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.
Lalpha.27 et ses artefacts restent disponibles. Les vérifications réelles
et leurs limites sont dans [le compte rendu](testing-alpha28.md).
+31
View File
@@ -13,6 +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 publiée — 12 septembre 2026
La reprise repart directement de lalpha.24 : relief et cavités conservés,
marais souterrains localisés, montgolfière retirée et navires fondés sur le
véritable template vanilla `minecraft:shipwreck/rightsideup_full`, restauré.
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
damorç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`.
Créer un **nouveau monde Sanctuary** pour cet essai. Les sauvegardes et réglages
personnels sont conservés. Aucun essai Windows natif nest revendiqué.
## Alpha.24 publiée — 11 septembre 2026
`check build assemblePack` et les essais natifs passent. Le JAR et
+105
View File
@@ -0,0 +1,105 @@
# Alpha.27 — vérifications de reprise
Base de travail : `e7d1f29` (alpha.24), branche
`codex/reprise-sanctuary-alpha27`. Mod et pack : `0.1.0-alpha.27`, Minecraft
`26.3-pre-2`. Nouveau monde Sanctuary, Moyen 724 par défaut.
Les fichiers de densité et les ressources de relief de la 24 sont conservés.
Les deux changements localisés concernent les biomes de marais et les lieux
aériens. Voir le [périmètre exact](generation-alpha27.md).
## Résultat local du 12 septembre 2026
`./gradlew check build` réussit en **3 min 28 s**. Le serveur de développement
termine ses **19 tests obligatoires**, dont les témoins marais et aériens, sans
échec. Le journal faisant foi est `build/alpha27-check-build-final.log` ; les
premiers journaux de préparation ne décrivent pas ce résultat final.
Commande exacte :
```sh
./gradlew check build -PsanctuaryDensityMaps=false -PsanctuaryAerialPreview -PsanctuaryAerial24Tests=true
./gradlew assemblePack -x :sanctuary:check
```
La première option désactive les images de densité facultatives. Le contrôle
`check` vient de réussir et nest pas répété pendant lassemblage du pack.
Les 56 fichiers de densité, relief et hydrologie recensés sont identiques à
la base `e7d1f29` (`build/alpha27-evidence/terrain-base.json`).
Les deux reçus natifs ci-dessous proviennent du même processus `48406`, dans
un **nouveau monde Moyen, diamètre 724, graine 0**, avec les structures et
l'option expérimentale activées. Ils ont été écrits à 00:32:38 et 00:32:47,
pendant le dernier passage réussi. Le générateur reste `sanctuary:island_v24`
dans cette reprise ; la version du mod est bien alpha.27.
## Marais souterrain réel
`Swamp27WorldGameTests` réutilise le plan hydrologique central déjà en cache,
puis inspecte au maximum huit chunks FULL demandés explicitement. Aucune
région supplémentaire n'est planifiée pour sélectionner les eaux témoins ;
aucun bloc ni point d'eau n'est posé par le test.
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/swamp27/native.json`
confirme :
- **1 444 sols de cavités sous voûte**, dont 1 315 couverts d'herbe, mousse ou boue ;
- **355 blocs d'eau** dans le biome de marais ;
- **494 plantes** : 107 buissons à lucioles, 292 herbes courtes et 95 lianes ;
- des eaux souterraines existantes à Y118, 120, 129 et 130 ; 289 colonnes d'eau
recoupent le champ du marais dans la région inspectée.
Repère pour visiter ce témoin dans un nouveau monde de mêmes paramètres :
```text
/tp @s -6.5 130 -92.5
```
`diagnostics/swamp27/blocks.json` exporte une fenêtre des **blocs FULL réels**
et une coupe destinée à l'aperçu. Le monde reste intact. Cette projection ne
reproduit pas l'éclairage, les shaders ou le rendu exact du client Minecraft.
## Quatre lieux aériens
Le reçu `mods/sanctuary/build/run/gameTest/diagnostics/aerial24/create.json`
porte `passed: true` et confirme **deux bateaux, deux îlots, deux marchands
réels, quatre coffres et une source de cascade**. Les deux marchands sont
observés après chargement effectif des entités. Les 56 chunks du témoin
aérien incluent les structures et leurs points de rivage.
Les coques lisent le vrai template Minecraft `shipwreck/rightsideup_full`,
palette 0 : 643 des 659 blocs physiques dorigine sont conservés lors de la
restauration, 16 remplacés et 76 ajoutés avant le mobilier marchand. Les
escaliers, dalles et la cabine native restent présents. Les aperçus de blocs
simplifiés ont été inspectés ; ils ne sont pas des captures du client.
Le test contrôle les blocs posés, les accès, les coffres enterrés des îlots,
les starts natifs et leur sérialisation. Il ouvre les quatre coffres,
déplace les deux marchands et retire quelques blocs dans le monde jetable,
puis sauvegarde. Les quatre sites restent enregistrés comme générés ; le
journal d'expansion est inchangé.
Les tests purs couvrent également **neuf combinaisons** de graines 0/42/2026
et diamètres 512/724/1024 : déterminisme des plans, orientations, limites et
réservations. Ce sont des calculs de plans et de géométrie, **pas neuf mondes
natifs intégralement générés**.
## Limites
Cette passe ne valide pas une nouvelle matrice de rechargements, toutes les
graines ni toute l'île. Elle ne garantit pas des grenouilles ou des chênes
dans chaque cavité. Le premier témoin sec a servi à corriger la sélection
du test ; les chiffres ci-dessus proviennent du témoin humide final.
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 damorçage vérifiés,
puis téléchargement anonyme des mêmes artefacts. Deux installations isolées
et deux synchronisations de linstance 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).
+99
View File
@@ -0,0 +1,99 @@
# 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 nest revendiqué. Le créateur a validé
visuellement la 27 ; les coupes de densité servent ici à contrôler cette
nouvelle passe, sans représenter leau, 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 dexpansion, 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.
Lexpansion de 512 conserve 1 299 échantillons contre 136 creusés.
Sur la grille de hauteurs, 54 colonnes hautes montent et 84 sabaissent.
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 sagit dun 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 nest 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, à lespace et aux quatre
appuis natifs. Le premier petit échantillon nen contient aucune : son audit
de placement ne constitue pas une preuve de présence en jeu.
- La source de la cascade est contrôlée ; son écoulement complet jusquau vide
nest pas mesuré par ce protocole. La propagation reste celle de Minecraft.
- Les témoins historiques purs restent vérifiés ; les comparaisons exactes de
lancien champ utilisent explicitement `legacy27()`. Elles ne prétendent pas
que le nouveau relief serait identique à celui de la 27.
- Il ne sagit pas dun benchmark client ni dune 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 nest 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.
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.24
pack_version=0.1.0-alpha.24
mod_version=0.1.0-alpha.28
pack_version=0.1.0-alpha.28
maven_group=fr.koka.sanctuary
+18
View File
@@ -603,6 +603,8 @@ tasks.register('aerial24Smoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.aerial.Aerial24Smoke'
if (providers.gradleProperty('sanctuaryAerialPreview').isPresent())
systemProperty('sanctuary.aerial.preview', rootProject.layout.buildDirectory.file('alpha27-preview/native-ships.json').get().asFile.absolutePath)
}
tasks.named('check') { dependsOn('aerial24Smoke') }
@@ -613,3 +615,19 @@ tasks.register('aerialPersistence24Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.aerial.AerialPersistence24Smoke'
}
tasks.named('check') { dependsOn('aerialPersistence24Smoke') }
tasks.register('swamp27VolumesSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
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') }
@@ -33,7 +33,7 @@ public final class Aerial24FlowAdmissionGameTests {
&& level.structureManager().shouldGenerateStructures(), "Fresh generation24 with aerial sites and native structures enabled");
var plan = generator.aerialPlan24();
var session = Objects.requireNonNull(ExpansionRuntime.session(level));
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 5, "Use an untouched disposable world for admission witnesses");
helper.assertTrue(session.islands().size() == 1 && plan.sites().size() == 4, "Use an untouched disposable world for admission witnesses");
var evidence = StructureCensusService.snapshot(level.getServer()).evidence();
String dimension = level.dimension().identifier().toString();
for (var site : plan.sites()) for (var chunk : site.claimedChunks())
@@ -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;
@@ -47,7 +47,7 @@ public final class Aerial24WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
@GameTest(maxTicks = 24000)
public void fiveSitesPersistAfterUse(GameTestHelper helper) throws Exception {
public void fourSitesPersistAfterUse(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
try { begin(helper, report); }
catch (Exception failure) { failure(report, failure); throw failure; }
@@ -69,10 +69,10 @@ public final class Aerial24WorldGameTests {
helper.assertTrue(StructureCensusService.aerialSites(level).isEmpty(), "Disabled aerial sites are not presented as planned");
report.put("passed", true); write(Path.of("diagnostics/aerial24/off.json"), report); helper.succeed(); return;
}
helper.assertTrue(plan.sites().size() == 5, "All five initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().size() == 4, "All four initial aerial sites have a finite saved plan");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2, "Two merchant ships");
helper.assertTrue(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "One waterfall site");
if (generator.initialDiameter() == 724 && level.getSeed() == 0)
if (generator.initialDiameter() == 724 && (level.getSeed() == 0 || level.getSeed() == 42))
AerialRenderer24.writeDiagnostic(Path.of("diagnostics/aerial24/blocks.json"), plan.sites());
report.put("geometry", JSON.toJsonTree(plan.sites())); report.put("claim_count", plan.claimsChunks().size());
report.put("census_before", StructureCensusService.aerialSites(level));
@@ -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;
}
@@ -148,8 +154,8 @@ public final class Aerial24WorldGameTests {
report.put("native_tags", tags); report.put("full_chunks", chunks.size());
report.put("blocks_after", countSites(level, plan));
var census = StructureCensusService.aerialSites(level);
helper.assertTrue(census.size() == 5 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Five historical generated sites remain indexed after loot, mining and demolition");
helper.assertTrue(census.size() == 4 && census.stream().allMatch(site -> site.status() == StructureCensusService.AerialStatus.GENERATED),
"Four historical generated sites remain indexed after loot, mining and demolition");
report.put("census_after", census);
helper.assertTrue(equalFiles(journalBefore, files(world.resolve("data").resolve(generator.journalDirectory()))), "Aerial tests never reserve or activate an expansion");
report.put("expansion_journal_unchanged", 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,19 +272,37 @@ 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 == 5 && waterSources == 1, "Five physical chests and exactly one planned source");
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));
report.put("vegetation", vegetation);
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,
@@ -305,8 +349,8 @@ public final class Aerial24WorldGameTests {
for (var entry : ores) { level.setBlock(entry.getKey(), Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(entry.getKey())); }
}
}
var balloon = plan.sites().stream().filter(site -> site.kind() == AerialPlan24.Kind.BALLOON).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(balloon).below();
var ship = plan.sites().stream().filter(AerialPlan24.Site::merchant).findFirst().orElseThrow();
var demolition = AerialGeometry24.entry(ship).below();
helper.assertTrue(level.getBlockState(demolition).isSolid(), "The demolition witness removes an existing floor");
level.setBlock(demolition, Blocks.AIR.defaultBlockState(), 2); removed.add(coordinates(demolition));
var extracted = new TreeMap<String, List<Double>>(); int i = 0;
@@ -0,0 +1,148 @@
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(12).toList();
var registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
var found = new LinkedHashMap<String, StructureStart>(); var observed = new TreeMap<String, Integer>();
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 (List.of("minecraft:mineshaft", "minecraft:mineshaft_mesa", "minecraft:swamp_hut").contains(id)) found.putIfAbsent(id, start);
}
}
report.put("candidate_chunks", candidates.size()); report.put("observed_starts", observed);
var full = new LinkedHashSet<ChunkPos>();
// Only a few actual pieces are visited: this is not full-map pregeneration.
for (var entry : found.entrySet()) {
full.add(entry.getValue().getChunkPos());
int remaining = entry.getKey().equals("minecraft:swamp_hut") ? 4 : 4;
for (var piece : entry.getValue().getPieces()) {
if (remaining-- <= 0) break;
var b = piece.getBoundingBox(); full.add(new ChunkPos(b.getCenter().getX() >> 4, b.getCenter().getZ() >> 4));
if (entry.getKey().equals("minecraft:swamp_hut")) b.intersectingChunks().forEach(full::add);
}
}
// 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>();
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>();
for (var piece : actual.getPieces()) {
var b = piece.getBoundingBox();
for (int x = b.minX(); x <= b.maxX(); x++) for (int z = b.minZ(); z <= b.maxZ(); z++) {
if (!full.contains(new ChunkPos(x >> 4, z >> 4))) continue;
for (int y = b.minY(); y <= b.maxY(); y++) {
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 (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()));
}
var rooms = new ArrayList<Object>();
for (var site : StructureCensusService.snapshot(level.getServer()).evidence().sites().values()) {
var key = site.key();
if (!key.id().equals(StructureCensusMemory.MONSTER_ROOM) || !key.dimension().equals(level.dimension().identifier().toString())) continue;
var cp = new ChunkPos(key.x() >> 4, key.z() >> 4);
if (!full.contains(cp)) continue;
var at = new BlockPos(key.x(), key.y(), key.z());
helper.assertTrue(level.getBlockEntity(at) instanceof SpawnerBlockEntity, "Successful vanilla monster_room has its real spawner at " + at);
rooms.add(Map.of("position", List.of(key.x(), key.y(), key.z()), "spawner", true));
}
report.put("structures", proofs); report.put("monster_rooms", rooms); report.put("full_chunks", full.size());
report.put("mineshaft_found", found.keySet().stream().anyMatch(id -> id.contains("mineshaft")));
report.put("swamp_hut_found", found.containsKey("minecraft:swamp_hut"));
report.put("monster_room_found", !rooms.isEmpty());
report.put("scope", "At most12 normal start candidates and24 FULL chunks; absence is reported, never replaced by synthetic fixtures");
}
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);
}
}
@@ -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();
}
}
@@ -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) {
@@ -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)),
@@ -0,0 +1,247 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.GsonBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.worldgen.PopulationGenerationContext;
import fr.koka.sanctuary.worldgen.PopulationHydrology;
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
import fr.koka.sanctuary.worldgen.IslandCapacity;
import fr.koka.sanctuary.worldgen.SanctuaryBiomeSource;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import fr.koka.sanctuary.worldgen.Swamp27Volumes;
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.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
/** One bounded native observation. Never places blocks, entities, or synthetic water. */
public final class Swamp27WorldGameTests {
private static final com.google.gson.Gson JSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path OUT = Path.of("diagnostics/swamp27");
private static final int MAX_COLUMNS = 256;
private static final int MAX_FULL_CHUNKS = 8;
@GameTest(maxTicks = 2400)
public void nativeUndergroundSwamp(GameTestHelper helper) throws Exception {
var report = new LinkedHashMap<String, Object>();
long started = System.nanoTime();
try {
ServerLevel level = helper.getLevel();
var generator = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
report.put("seed", level.getSeed()); report.put("diameter", generator.initialDiameter());
report.put("generator", generator.rootGeneration()); report.put("process_id", ProcessHandle.current().pid());
report.put("generate_structures", level.structureManager().shouldGenerateStructures());
report.put("experimental", generator.experimentalStructures());
helper.assertTrue(generator.rootGeneration() == 24, "Alpha27 uses the restored24 terrain generator");
var binding = Objects.requireNonNull(PopulationGenerationContext.of(level));
var resolver = generator.source().createResolver(binding.random().createClimateSampler(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build()));
var candidates = waterCandidates(helper, binding, resolver, report);
int columns = 0;
// Deterministic rings visit the middle of distinct chunks, with a strict sampler budget.
int radius = Math.min(8, generator.initialDiameter() / 48);
search: for (int ring = 0; ring <= radius; ring++)
for (int cx = -ring; cx <= ring; cx++) for (int cz = -ring; cz <= ring; cz++) {
if (Math.max(Math.abs(cx), Math.abs(cz)) != ring) continue;
if (columns++ >= MAX_COLUMNS) break search;
int x = cx * 16 + 8, z = cz * 16 + 8;
NoiseColumn column = binding.generator().getBaseColumn(x, z, level, binding.random());
for (int y = Swamp27Volumes.MAX_Y - 2; y >= Swamp27Volumes.MIN_Y; y--) {
if (!column.getBlock(y).isAir() || !column.getBlock(y + 1).isAir()
|| !natural(column.getBlock(y - 1))) continue;
if (!resolver.getNoiseBiome(x >> 2, y >> 2, z >> 2).is(SanctuaryBiomeSource.SWAMP27)) continue;
int roof = roof(column, y);
if (roof < 0) continue;
candidates.putIfAbsent(new ChunkPos(cx, cz), new BlockPos(x, y, z));
break;
}
if (candidates.size() == MAX_FULL_CHUNKS) break search;
}
report.put("sampled_columns", Math.min(columns, MAX_COLUMNS));
report.put("candidate_positions", candidates.values().stream().map(Swamp27WorldGameTests::xyz).toList());
helper.assertTrue(!candidates.isEmpty(), "The bounded biome/terrain scan finds a real cave floor below rock");
var pending = candidates.keySet().iterator();
var loaded = new ArrayList<ChunkPos>();
Runnable[] step = new Runnable[1];
step[0] = () -> {
try {
if (pending.hasNext()) {
var chunk = pending.next(); report.put("stage", "FULL " + chunk);
level.getChunk(chunk.x(), chunk.z(), ChunkStatus.FULL, true); loaded.add(chunk);
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run()); return;
}
report.put("stage", "inspect actual cave floors and vegetation");
inspect(helper, level, loaded, report);
report.put("explicit_full_chunks", loaded.size());
report.put("full_chunk_limit", MAX_FULL_CHUNKS);
report.put("elapsed_seconds", (System.nanoTime() - started) / 1_000_000_000.0);
report.put("passed", true); write(OUT.resolve("native.json"), report); helper.succeed();
} catch (Exception error) {
failure(report, error); throw new IllegalStateException("Swamp27 native witness failed", error);
}
};
helper.runAtTickTime(helper.getTick() + 1, () -> step[0].run());
} catch (Exception error) { failure(report, error); throw error; }
}
private static LinkedHashMap<ChunkPos, BlockPos> waterCandidates(GameTestHelper helper,
PopulationGenerationContext.Binding binding, BiomeResolver resolver, Map<String, Object> report) {
int before = PopulationHydrologyRuntime.cachedRegionCount(binding.random());
var plan = PopulationHydrologyRuntime.regionPlan(binding.generator(), binding.random(), new IslandCapacity.Region(0, 0)).water();
int after = PopulationHydrologyRuntime.cachedRegionCount(binding.random());
helper.assertTrue(before == after, "The water witness reuses the already cached central region");
var undergroundIds = new HashSet<Long>();
plan.features().stream().filter(feature -> feature.kind() == PopulationHydrology.Kind.TERRACE)
.forEach(feature -> undergroundIds.add(feature.id()));
var allY = new TreeMap<Integer, Integer>(); var lowerY = new TreeMap<Integer, Integer>();
var byChunk = new HashMap<ChunkPos, Integer>(); var positions = new HashMap<ChunkPos, BlockPos>();
int intersections = 0, lowerIntersections = 0, waterTopIntersections = 0;
for (var cell : plan.cells()) {
if (!cell.hasWater()) continue;
allY.merge(cell.waterY(), 1, Integer::sum);
boolean lower = undergroundIds.contains(cell.featureId());
if (lower) lowerY.merge(cell.waterY(), 1, Integer::sum);
boolean inWater = resolver.getNoiseBiome(cell.x() >> 2, cell.waterY() >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27);
boolean aboveWater = resolver.getNoiseBiome(cell.x() >> 2, (cell.waterY() + 1) >> 2, cell.z() >> 2).is(SanctuaryBiomeSource.SWAMP27);
if (inWater) { intersections++; if (lower) lowerIntersections++; }
if (aboveWater) waterTopIntersections++;
if (!lower || !inWater && !aboveWater) continue;
var chunk = new ChunkPos(cell.x() >> 4, cell.z() >> 4);
byChunk.merge(chunk, 1, Integer::sum);
positions.putIfAbsent(chunk, new BlockPos(cell.x(), cell.waterY() + 1, cell.z()));
}
var selected = new LinkedHashMap<ChunkPos, BlockPos>();
byChunk.keySet().stream().sorted(Comparator.<ChunkPos>comparingInt(chunk -> byChunk.get(chunk)).reversed()
.thenComparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).limit(4)
.forEach(chunk -> selected.put(chunk, positions.get(chunk)));
var evidence = new LinkedHashMap<String, Object>();
evidence.put("region", List.of(0, 0)); evidence.put("new_regions_planned", after - before);
evidence.put("all_water_columns_by_surface_y", allY);
evidence.put("underground_terrace_water_columns_by_surface_y", lowerY);
evidence.put("swamp_band", List.of(Swamp27Volumes.MIN_Y, Swamp27Volumes.MAX_Y - 1));
evidence.put("water_surface_biome_intersections", intersections);
evidence.put("underground_water_surface_biome_intersections", lowerIntersections);
evidence.put("above_water_biome_intersections", waterTopIntersections);
evidence.put("prioritized_chunks", selected.keySet().stream().map(chunk -> List.of(chunk.x(), chunk.z())).toList());
evidence.put("scope", "Existing central region only; zeros are observations, not a required water quota");
report.put("cached_water_selection", evidence);
return selected;
}
private static void inspect(GameTestHelper helper, ServerLevel level, List<ChunkPos> chunks,
Map<String, Object> report) throws Exception {
int floors = 0, covered = 0, plants = 0, water = 0;
BlockPos witness = null; int bestScore = -1, witnessRoof = -1;
var counts = new TreeMap<String, Integer>();
var byChunk = new ArrayList<Map<String, Object>>();
for (var chunk : chunks) {
int chunkFloors = 0, chunkPlants = 0, chunkWater = 0;
for (int x = chunk.x() * 16; x < chunk.x() * 16 + 16; x++)
for (int z = chunk.z() * 16; z < chunk.z() * 16 + 16; z++)
for (int y = Swamp27Volumes.MIN_Y; y < Swamp27Volumes.MAX_Y; y++) {
var at = new BlockPos(x, y, z);
if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue;
var state = level.getBlockState(at);
if (state.getFluidState().is(FluidTags.WATER)) { water++; chunkWater++; }
if (plant(state)) {
// A feature must obey native placement; old vanilla vines elsewhere are irrelevant.
if (!state.is(Blocks.VINE)) helper.assertTrue(state.canSurvive(level, at), "Native swamp plant retains its support: " + at + " " + state);
plants++; chunkPlants++; counts.merge(id(state), 1, Integer::sum);
}
if ((!state.isAir() && !plant(state)) || !state.getFluidState().isEmpty()) continue;
var ground = level.getBlockState(at.below());
if (!natural(ground)) continue;
int ceiling = roof(level, at);
if (ceiling < 0 || !level.getBlockState(at.above()).getCollisionShape(level, at.above()).isEmpty()) continue;
floors++; chunkFloors++;
boolean soil = ground.is(Blocks.MOSS_BLOCK) || ground.is(Blocks.GRASS_BLOCK) || ground.is(Blocks.MUD);
if (soil) covered++;
int score = (waterNearby(level, at.below(), chunk) ? 40 : 0)
+ (plant(state) ? 20 : 0) + (soil ? 5 : 0) - Math.max(0, ceiling - y - 24);
// Keep the export within this already loaded chunk, without another request.
if (score > bestScore && (x & 15) >= 3 && (x & 15) <= 12 && (z & 15) >= 3 && (z & 15) <= 12) {
witness = at; bestScore = score; witnessRoof = ceiling;
}
}
byChunk.add(Map.of("chunk", List.of(chunk.x(), chunk.z()), "cave_floors", chunkFloors,
"plants", chunkPlants, "water_blocks", chunkWater));
}
report.put("native_cave_floors", floors); report.put("covered_floors", covered);
report.put("plants", plants); report.put("plant_counts", counts); report.put("water_blocks", water);
report.put("chunks", byChunk);
helper.assertTrue(floors > 0 && covered > 0, "FULL contains the new swamp biome over actual grass/moss/mud cave floors with a rock ceiling");
helper.assertTrue(plants > 0, "The bounded FULL witness contains naturally generated swamp vegetation");
helper.assertTrue(witness != null, "A visitable cave witness exists inside the loaded footprint");
report.put("visit", xyz(witness)); report.put("ceiling_y", witnessRoof);
report.put("teleport", "/tp @s " + (witness.getX() + .5) + " " + witness.getY() + " " + (witness.getZ() + .5));
export(level, witness, witnessRoof, report);
}
private static void export(ServerLevel level, BlockPos witness, int ceiling, Map<String, Object> report) throws Exception {
int minX = (witness.getX() >> 4) * 16, minZ = (witness.getZ() >> 4) * 16;
int minY = Math.max(0, witness.getY() - 3), maxY = Math.min(ceiling + 1, witness.getY() + 32);
var blocks = new ArrayList<List<Object>>(); var cutaway = new ArrayList<List<Object>>();
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) for (int y = minY; y <= maxY; y++) {
BlockState state = level.getBlockState(new BlockPos(x, y, z));
if (state.isAir()) continue;
List<Object> block = List.of(x - minX, y - minY, z - minZ, id(state)); blocks.add(block);
// A documented display cut only; the actual world and the full export stay intact.
if (y < witness.getY() || z >= witness.getZ() && y < ceiling) cutaway.add(block);
}
var origin = List.of(minX, minY, minZ); var size = List.of(16, maxY - minY + 1, 16);
write(OUT.resolve("blocks.json"), List.of(
Map.of("id", "swamp27_native_full", "size", size, "origin", origin, "blocks", blocks),
Map.of("id", "swamp27_native_cutaway", "size", size, "origin", origin, "blocks", cutaway)));
report.put("export", Map.of("path", "diagnostics/swamp27/blocks.json", "origin", origin, "size", size,
"blocks", blocks.size(), "cutaway_blocks", cutaway.size(), "source", "actual FULL blocks; no test-authored terrain"));
}
private static int roof(NoiseColumn column, int y) {
for (int top = y + 3; top <= Math.min(280, y + 96); top++) if (natural(column.getBlock(top))) return top;
return -1;
}
private static boolean waterNearby(ServerLevel level, BlockPos floor, ChunkPos chunk) {
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
var at = floor.offset(dx, 0, dz);
if (at.getX() >> 4 != chunk.x() || at.getZ() >> 4 != chunk.z()) continue;
if (level.getFluidState(at).is(FluidTags.WATER)) return true;
}
return false;
}
private static int roof(ServerLevel level, BlockPos at) {
for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++)
if (natural(level.getBlockState(at.atY(y)))) return y;
return -1;
}
private static boolean plant(BlockState state) {
return state.is(Blocks.SHORT_GRASS) || state.is(Blocks.FIREFLY_BUSH) || state.is(Blocks.SUGAR_CANE)
|| state.is(Blocks.LILY_PAD) || state.is(Blocks.VINE);
}
private static boolean natural(BlockState state) {
return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF) || state.is(Blocks.CALCITE)
|| state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.DIRT)
|| state.is(Blocks.COARSE_DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT)
|| state.is(Blocks.PODZOL) || state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK)
|| state.is(Blocks.MUD) || state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY);
}
private static List<Integer> xyz(BlockPos at) { return List.of(at.getX(), at.getY(), at.getZ()); }
private static String id(BlockState state) { return BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(); }
private static void write(Path path, Object value) throws Exception {
Files.createDirectories(path.getParent()); Files.writeString(path, JSON.toJson(value) + "\n");
}
private static void failure(Map<String, Object> report, Exception error) {
report.put("passed", false); report.put("error", error.toString());
try { write(OUT.resolve("failure.json"), report); } catch (Exception io) { error.addSuppressed(io); }
SanctuaryMod.LOGGER.error("Swamp27 native witness failed", error);
}
}
@@ -6,7 +6,7 @@
"environment": "*",
"license": "GPL-3.0-or-later",
"entrypoints": {
"fabric-gametest": ["${gametest_entrypoint}", "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.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"],
@@ -86,6 +86,7 @@ public final class SanctuaryMod implements ModInitializer {
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("swamp27_decoration"), fr.koka.sanctuary.worldgen.Swamp27Decoration.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_decorations"), PopulationDecorationsFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_shore_sugar_cane"), PopulationShoreSugarCaneFeature.CODEC);
@@ -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);
}
@@ -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,14 @@ 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");
int variants = hut ? hutHeights.size() : entry.habitat() == Habitat.UNDERGROUND || entry.family().equals("ruined_portal") ? 3 : 1;
for (int vertical = 0; vertical < variants; vertical++) {
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 +221,18 @@ 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; }
// 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);
@@ -333,17 +347,35 @@ public final class ExpansionStructures23 {
|| 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()) {
if (island.origin() && !entry.family().equals("swamp_hut")) {
var binding = city.starterContext();
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 (!MineshaftSupport28.anchored(piece, (x, z) -> ground.at(x, z).column)) return "mineshaft-no-native-support";
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;
@@ -474,7 +506,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 +525,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 +539,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 +576,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,32 @@
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) {
var box = piece.getBoundingBox();
int length = piece.getOrientation().getAxis() == net.minecraft.core.Direction.Axis.Z ? box.getZSpan() : box.getXSpan();
for (int z : length > 5 ? new int[]{2, length - 3} : new int[]{2}) for (int x : new int[]{0, 2}) {
var pier = SwampHutPlacement28.worldPosition(piece, x, -1, z);
var column = columns.apply(pier.getX(), pier.getZ()); boolean anchor = false;
// Native wooden support searches at most20 below its floor plank.
for (int y = pier.getY() - 1; y >= pier.getY() - 20; y--) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
}
// Its alternative is a chain up to50 blocks, anchored above the excavated corridor.
if (!anchor) for (int y = box.maxY() + 1; y <= pier.getY() + 50; y++) {
var state = column.getBlock(y);
if (!state.getFluidState().isEmpty()) break;
if (state.isSolid()) { anchor = true; break; }
}
if (!anchor) return false;
}
return true;
}
}
@@ -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;
}
}
@@ -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) {
@@ -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);
}
}
}
}
@@ -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,8 @@ 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) 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 +21,85 @@ 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)
).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);
}
@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;
float terrainValue;
double weight = geology28 ? CavesRelief28.deformationWeight(x, y, z, capacity.radius()) : 0;
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));
terrainValue = terrainAt(sample, x - shift.x(), y - 64 - shift.y(), z - shift.z());
} else terrainValue = raw.sampleValue(sample, x, y - 64, z);
// Keep the island envelope at actual coordinates, independent from displaced rock.
int localY = y - 64;
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
float natural = PopulationIslandShape.raisedDensity(x, y, z, terrainValue,
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
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);
}
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 +108,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);
}
@Override public Interval range() { return Interval.of(-1, 1); }
@Override public int domainAxes() { return ALL_AXES; }
@@ -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
@@ -16,13 +16,15 @@ import net.minecraft.world.level.biome.Climate;
/** Alpha.13 keeps Population's origin woodlands and the laboratory's native cave pockets.
* Child islands retain the laboratory's complete climate palette. */
public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
public static final ResourceKey<Biome> SWAMP27 = ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
fr.koka.sanctuary.SanctuaryMod.id("swamp27_caves"));
public static final List<String> REQUIRED_BIOMES = Stream.concat(
ExpansionBiomeSource.REQUIRED_BIOMES.stream(), Stream.of(
PopulationIslandBiomeSource.OAK_FOREST, PopulationIslandBiomeSource.BIRCH_FOREST,
PopulationIslandBiomeSource.CLEARING, PopulationIslandBiomeSource.DRY_WOODLAND,
PopulationIslandBiomeSource.ROCKY_HEATH, PopulationIslandBiomeSource.DARK_GROVE,
PopulationIslandBiomeSource.BAMBOO_GROVE, PopulationIslandBiomeSource.SULFUR_DEPTHS,
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES
PopulationIslandBiomeSource.LUSH_CAVES, PopulationIslandBiomeSource.DRIPSTONE_CAVES, SWAMP27
).map(key -> key.identifier().toString())).toList();
public static final MapCodec<SanctuaryBiomeSource> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
@@ -71,6 +73,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
private final boolean cities;
private final int generation;
private final Holder<Biome> cityBiome;
private final Holder<Biome> swamp27;
private volatile java.util.Map<String, fr.koka.sanctuary.worldgen.city.LostCityPlan> cityPlans = java.util.Map.of();
public SanctuaryBiomeSource(List<Holder<Biome>> biomes) {
@@ -81,6 +84,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
this.generation = generation;
this.cities = ExpansionIsland.spatialGeneration(generation);
allBiomes = List.copyOf(biomes);
swamp27 = generation == 24 ? required(SWAMP27) : null;
if ((generation == 23 || generation == 24)) for (String name : fr.koka.sanctuary.expansion.ExpansionOcean23.BIOMES)
required(ResourceKey.create(net.minecraft.core.registries.Registries.BIOME,
net.minecraft.resources.Identifier.withDefaultNamespace(name)));
@@ -122,6 +126,20 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
// Reuse the historical selector itself: its noise, thresholds, order and child
// climate choices stay exact rather than being approximated by another climate field.
Holder<Biome> laboratory = super.resolve(snapshot, x, y, z);
if (generation == 24 && y >= Swamp27Volumes.MIN_Y && y < Swamp27Volumes.MAX_Y) {
for (ExpansionIsland island : snapshot) {
if (!island.origin() || !island.contains(x, z)) continue;
Holder<Biome> lower = woodlands.getNoiseBiome(quartX, quartY, quartZ);
boolean cave = laboratory.is(Biomes.LUSH_CAVES) || laboratory.is(Biomes.DRIPSTONE_CAVES)
|| laboratory.is(Biomes.SULFUR_CAVES) || lower.is(PopulationIslandBiomeSource.LUSH_CAVES)
|| lower.is(PopulationIslandBiomeSource.DRIPSTONE_CAVES)
|| lower.is(PopulationIslandBiomeSource.SULFUR_DEPTHS)
|| lower.is(PopulationIslandBiomeSource.DARK_GROVE);
if (cave && Swamp27Volumes.contains(island.seed(), island.diameter(),
x - island.centerX(), y, z - island.centerZ())) return swamp27;
break;
}
}
if (y < 0 || y >= 384 || laboratory.is(Biomes.LUSH_CAVES)
|| laboratory.is(Biomes.DRIPSTONE_CAVES) || laboratory.is(Biomes.SULFUR_CAVES)) return laboratory;
for (ExpansionIsland island : snapshot) {
@@ -183,7 +183,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 +215,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(
@@ -0,0 +1,138 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.tags.FluidTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VineBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
/** Decorates existing floors, water and ceilings only. Writes never leave the current chunk. */
public final class Swamp27Decoration implements Feature {
public static final MapCodec<Swamp27Decoration> CODEC = MapCodec.unit(Swamp27Decoration::new);
@Override public MapCodec<Swamp27Decoration> codec() { return CODEC; }
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator host)
|| host.rootGeneration() != 24) return false;
var binding = PopulationGenerationContext.of(generator, level.getLevel());
if (binding == null) return false;
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
long seed = binding.random().seed();
boolean changed = false, frogPlaced = false;
int trees = 0;
boolean frogOpportunity = Math.floorMod(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0xF207L), 8) == 0;
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
int floors = 0;
for (int y = Swamp27Volumes.MAX_Y - 1; y >= Swamp27Volumes.MIN_Y; y--) {
BlockPos at = new BlockPos(x, y, z);
if (!level.getBlockState(at).isAir() || !level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27)) continue;
BlockPos floor = at.below();
BlockState ground = level.getBlockState(floor);
boolean water = ground.getFluidState().is(FluidTags.WATER) && ground.getFluidState().isSource();
if (!water && !natural(ground)) continue;
if (!roof(level, at) || lavaNearby(level, at)) continue;
long local = WoodlandGroveGeometry.seed(seed, x, y, z, 0xB07527L);
RandomSource detail = RandomSource.create(local);
if (water) {
if (detail.nextInt(9) == 0) changed |= plant(level, at, Blocks.LILY_PAD.defaultBlockState());
if (++floors >= 3) break;
continue;
}
// Replace a single already solid natural floor, never a support in air or an ore.
boolean wet = waterNearby(level, floor, 2);
long patch = WoodlandGroveGeometry.seed(seed, Math.floorDiv(x, 6), y / 5, Math.floorDiv(z, 6), 0x501127L);
BlockState soil = (wet ? Blocks.MUD : Math.floorMod(patch, 5) < 2
? Blocks.MOSS_BLOCK : Blocks.GRASS_BLOCK).defaultBlockState();
changed |= level.setBlock(floor, soil, 2);
if (trees < 2 && (x & 15) >= 4 && (x & 15) <= 11 && (z & 15) >= 4 && (z & 15) <= 11
&& Math.floorMod(local, 23) == 0 && at.getY() + 9 < Swamp27Volumes.MAX_Y) {
// Native placement decides whether the trunk and crown fit an irregular room.
// A radius-four margin keeps every write inside this chunk; existing hydrology
// and structure protection still guard each native write.
if (level.setBlock(floor, Blocks.GRASS_BLOCK.defaultBlockState(), 2)
&& level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(TreeFeatures.SWAMP_OAK)
.value().place(level, generator, RandomSource.create(local), at)) {
trees++;
changed = true;
}
}
if (!level.getBlockState(at).isAir()) continue;
if (wet && detail.nextInt(5) == 0) changed |= plant(level, at, Blocks.SUGAR_CANE.defaultBlockState());
else if (detail.nextInt(13) == 0) changed |= plant(level, at, Blocks.FIREFLY_BUSH.defaultBlockState());
else if (detail.nextInt(4) == 0) changed |= plant(level, at, Blocks.SHORT_GRASS.defaultBlockState());
if (!frogPlaced && frogOpportunity && wet && level.getBlockState(at).isAir()
&& level.getBlockState(at.above()).isAir()
&& !level.getBlockState(floor).getCollisionShape(level, floor).isEmpty()) {
var frog = EntityTypes.FROG.create(level.getLevel(), EntitySpawnReason.CHUNK_GENERATION);
if (frog != null) {
frog.snapTo(x + .5, y, z + .5, detail.nextFloat() * 360, 0);
frog.finalizeSpawn(level, level.getCurrentDifficultyAt(at), EntitySpawnReason.CHUNK_GENERATION, null);
frog.setPersistenceRequired();
level.addFreshEntityWithPassengers(frog);
frogPlaced = true;
changed = true;
}
}
if (++floors >= 3) break;
}
}
// Short vines cling to actual walls; every segment must still have its own support.
for (int attempt = 0; attempt < 40; attempt++) {
RandomSource vines = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, attempt, minZ, 0x710E27L));
BlockPos at = new BlockPos(minX + 1 + vines.nextInt(14),
Swamp27Volumes.MIN_Y + vines.nextInt(Swamp27Volumes.MAX_Y - Swamp27Volumes.MIN_Y - 4),
minZ + 1 + vines.nextInt(14));
if (!level.getBiome(at).is(SanctuaryBiomeSource.SWAMP27) || !roof(level, at)) continue;
for (Direction face : Direction.Plane.HORIZONTAL) {
if (!natural(level.getBlockState(at.relative(face)))) continue;
BlockState vine = Blocks.VINE.defaultBlockState().setValue(VineBlock.getPropertyForFace(face), true);
for (int n = 0; n < 4 && at.getY() - n >= Swamp27Volumes.MIN_Y; n++) {
BlockPos segment = at.below(n);
if (!level.getBlockState(segment).isAir()
|| !level.getBiome(segment).is(SanctuaryBiomeSource.SWAMP27)
|| !natural(level.getBlockState(segment.relative(face))) || lavaNearby(level, segment)) break;
changed |= plant(level, segment, vine);
}
break;
}
}
return changed;
}
private static boolean plant(WorldGenLevel level, BlockPos at, BlockState plant) {
return level.getBlockState(at).isAir() && plant.canSurvive(level, at) && level.setBlock(at, plant, 2);
}
private static boolean roof(WorldGenLevel level, BlockPos at) {
for (int y = at.getY() + 3; y <= Math.min(280, at.getY() + 96); y++)
if (natural(level.getBlockState(new BlockPos(at.getX(), y, at.getZ())))) return true;
return false;
}
private static boolean waterNearby(WorldGenLevel level, BlockPos floor, int radius) {
for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++)
if (level.getFluidState(floor.offset(dx, 0, dz)).is(FluidTags.WATER)) return true;
return false;
}
private static boolean lavaNearby(WorldGenLevel level, BlockPos at) {
for (Direction face : Direction.values()) if (level.getFluidState(at.relative(face)).is(FluidTags.LAVA)) return true;
return false;
}
static boolean natural(BlockState state) {
return state.is(Blocks.STONE) || state.is(Blocks.DEEPSLATE) || state.is(Blocks.TUFF)
|| state.is(Blocks.CALCITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.GRANITE)
|| state.is(Blocks.DIORITE) || state.is(Blocks.DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL)
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.MOSS_BLOCK) || state.is(Blocks.MUD)
|| state.is(Blocks.PACKED_MUD) || state.is(Blocks.CLAY);
}
}
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.worldgen;
/** A biome field only: never a density, aquifer, heightmap or terrain mutation. */
public final class Swamp27Volumes {
public static final int MIN_Y = 72;
public static final int MAX_Y = 188;
private Swamp27Volumes() {}
public static boolean contains(long seed, int diameter, int x, int y, int z) {
if (y < MIN_Y || y >= MAX_Y || diameter <= 0) return false;
double radius = diameter * .46;
if ((double) x * x + (double) z * z > radius * radius) return false;
double verticalEdge = Math.min(1, Math.min((y - MIN_Y) / 16.0, (MAX_Y - y) / 16.0));
// 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 > .25 + (1 - verticalEdge) * .7;
}
private static double noise(long seed, double x, double y, double z) {
int ix = (int) Math.floor(x), iy = (int) Math.floor(y), iz = (int) Math.floor(z);
double u = fade(x - ix), v = fade(y - iy), w = fade(z - iz);
double low = lerp(w, lerp(u, point(seed, ix, iy, iz), point(seed, ix + 1, iy, iz)),
lerp(u, point(seed, ix, iy, iz + 1), point(seed, ix + 1, iy, iz + 1)));
double high = lerp(w, lerp(u, point(seed, ix, iy + 1, iz), point(seed, ix + 1, iy + 1, iz)),
lerp(u, point(seed, ix, iy + 1, iz + 1), point(seed, ix + 1, iy + 1, iz + 1)));
return lerp(v, low, high);
}
private static double point(long seed, int x, int y, int z) {
long h = seed ^ x * 0x632BE59BD9B4E019L ^ y * 0x9E3779B97F4A7C15L ^ z * 0x94D049BB133111EBL;
h = (h ^ h >>> 30) * 0xBF58476D1CE4E5B9L;
h = (h ^ h >>> 27) * 0x94D049BB133111EBL;
return ((h ^ h >>> 31) >>> 11) * 0x1.0p-52 - 1;
}
private static double fade(double t) { return t * t * (3 - 2 * t); }
private static double lerp(double t, double a, double b) { return a + (b - a) * t; }
}
@@ -14,8 +14,7 @@ public final class AerialGeometry24 {
}
public static BlockPos chest(AerialPlan24.Site s) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2 - 2, 2, s.depth() / 2 + 2);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 22);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 4, 24);
case MINERAL_ISLET -> {
int x = s.width() / 2 + 4, z = s.depth() / 2 + 2;
yield world(s, x, terrainTop(s, x, z) - 3, z);
@@ -24,27 +23,25 @@ public final class AerialGeometry24 {
}
public static BlockPos occupant(AerialPlan24.Site s) {
if (!s.merchant()) throw new IllegalArgumentException("Only merchant ships have occupants");
return world(s, 5, 6, 20);
return world(s, 5, 4, 22);
}
public static UUID occupantUuid(AerialPlan24.Site s) {
return UUID.nameUUIDFromBytes(("sanctuary:aerial24:" + s.id() + ":" + s.seed() + ":merchant")
.getBytes(StandardCharsets.UTF_8));
}
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 7, 6, 19); }
public static BlockPos workstation(AerialPlan24.Site s) { return world(s, 3, 4, 23); }
public static BlockPos waterfallSource(AerialPlan24.Site s) {
if (!s.waterfall()) throw new IllegalArgumentException("This site has no waterfall");
return world(s, s.width() / 2, rockHeight(s) - 3, 3);
}
public static BlockPos entry(AerialPlan24.Site s) {
return switch (s.kind()) {
case BALLOON -> world(s, s.width() / 2, 2, s.depth() / 2 - 3);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 7, 6, 12);
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> world(s, 3, 5, 13);
case MINERAL_ISLET -> world(s, s.width() / 2, terrainTop(s, s.width() / 2, s.depth() / 2) + 1, s.depth() / 2);
};
}
public static List<BlockPos> interiorChecks(AerialPlan24.Site s) {
if (s.merchant()) return List.of(entry(s), world(s, 5, 6, 16), occupant(s), world(s, 5, 6, 23));
if (s.kind() == AerialPlan24.Kind.BALLOON) return List.of(entry(s), world(s, s.width() / 2, 2, s.depth() / 2));
if (s.merchant()) return List.of(entry(s), occupant(s), world(s, 5, 4, 21), world(s, 3, 4, 24));
return List.of(entry(s));
}
public static int terrainTop(AerialPlan24.Site s, int x, int z) {
@@ -19,17 +19,16 @@ public final class AerialPlan24 {
private final List<Site> sites;
private final Set<Long> claims;
public enum Kind { BALLOON, MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
public enum Kind { MERCHANT_PROVISIONS, MERCHANT_TOOLS, MINERAL_ISLET }
public AerialPlan24(List<Site> sites) {
this.sites = List.copyOf(sites);
if (!sites.isEmpty() && (sites.size() != 5
|| sites.stream().filter(s -> s.kind() == Kind.BALLOON).count() != 1
if (!sites.isEmpty() && (sites.size() != 4
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_PROVISIONS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MERCHANT_TOOLS).count() != 1
|| sites.stream().filter(s -> s.kind() == Kind.MINERAL_ISLET).count() != 2
|| sites.stream().filter(Site::waterfall).count() != 1))
throw new IllegalArgumentException("An aerial plan contains all five sites and exactly one waterfall, or is disabled");
throw new IllegalArgumentException("An aerial plan contains all four sites and exactly one waterfall, or is disabled");
var ids = new HashSet<String>();
var columns = new HashSet<Long>();
for (Site site : sites) {
@@ -70,8 +69,7 @@ public final class AerialPlan24 {
|| waterfall && kind != Kind.MINERAL_ISLET)
throw new IllegalArgumentException("Invalid aerial site geometry");
if (kind == Kind.MINERAL_ISLET && (width < 40 || depth < 40 || height < 20 + VEGETATION_HEADROOM)
|| kind == Kind.BALLOON && height != 28
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 27 || height != 18))
|| (kind == Kind.MERCHANT_PROVISIONS || kind == Kind.MERCHANT_TOOLS) && (width != 11 || depth != 28 || height != 18))
throw new IllegalArgumentException("Aerial site differs from its version24 gabarit");
int worldWidth = facing % 2 == 0 ? width : depth;
int worldDepth = facing % 2 == 0 ? depth : width;
@@ -15,6 +15,8 @@ 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);
@@ -40,41 +42,83 @@ public final class AerialPlanner24 {
var probe = new Probe(terrain, radius);
var sites = new ArrayList<Site>();
var claimed = new HashSet<Long>();
Kind[] kinds = {Kind.BALLOON, Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"balloon", "merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
Kind[] kinds = {Kind.MERCHANT_PROVISIONS, Kind.MERCHANT_TOOLS, Kind.MINERAL_ISLET, Kind.MINERAL_ISLET};
String[] ids = {"merchant_provisions", "merchant_tools", "mineral_islet_water", "mineral_islet_dry"};
double initialAngle = unit(mix(seed ^ 0xA24AE21AL)) * Math.PI * 2;
for (int ordinal = 0; ordinal < kinds.length; ordinal++) {
long siteSeed = ExpansionIsland.deriveSeed(seed ^ 0xA24AE21AL, ids[ordinal]);
int width = kinds[ordinal] == Kind.BALLOON ? 15 : ordinal < 3 ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int depth = kinds[ordinal] == Kind.BALLOON ? 19 : ordinal < 3 ? 27 : width;
int bodyHeight = kinds[ordinal] == Kind.BALLOON ? 28 : ordinal < 3 ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (ordinal >= 3 ? AerialPlan24.VEGETATION_HEADROOM : 0);
boolean merchant = ordinal < 2;
int width = merchant ? 11 : 40 + 4 * (int) Math.floorMod(siteSeed, 7);
int depth = merchant ? 28 : width;
int bodyHeight = merchant ? 18 : 20 + (int) Math.floorMod(mix(siteSeed), 16);
int height = bodyHeight + (merchant ? 0 : AerialPlan24.VEGETATION_HEADROOM);
Site accepted = null;
for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SITE; attempt++) {
long draw = mix(siteSeed + attempt * 0x9e3779b97f4a7c15L);
double angle = initialAngle + ordinal * (Math.PI * 2 / 5) + (unit(draw) - .5) * .9
// 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 = ordinal == 0 ? Math.clamp(shore.y + 24, 286, 321)
: ordinal < 3 ? 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 1632 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 = new Shore(cx, top + 1, cz);
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 == 3);
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 + (ordinal == 0 ? 18 : bodyHeight - 4), cz)) continue;
width, height, depth, facing, siteSeed, shore.x, shore.y, shore.z, ordinal == 2);
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");
@@ -88,6 +132,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.
@@ -137,6 +214,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();
}
@@ -41,7 +41,12 @@ public final class AerialRenderer24 {
BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString(), state.toString());
}).toList();
result.add(Map.of("id", site.id(), "kind", site.kind().name(), "min", List.of(site.minX(), site.minY(), site.minZ()),
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list));
"size", List.of(site.worldWidth(), site.height(), site.worldDepth()), "seed", site.seed(), "facing", site.facing(), "blocks", list,
"native_template", site.merchant() ? Map.of("id", NativeMerchantShip27.TEMPLATE, "palette", NativeMerchantShip27.PALETTE,
"native_blocks", NativeMerchantShip27.restored().nativeBlocks(), "preserved_blocks", NativeMerchantShip27.restored().preservedBlocks(),
"added_blocks", NativeMerchantShip27.restored().addedBlocks(), "replaced_blocks", NativeMerchantShip27.restored().replacedBlocks(),
"drained_blocks", NativeMerchantShip27.restored().drainedBlocks(),
"restoration", "Native hull and stern cabin; repaired side planks/windows, mast and sail; merchant bed/workstation/light added afterwards") : Map.of()));
}
if (path.toAbsolutePath().getParent() != null) Files.createDirectories(path.toAbsolutePath().getParent());
Files.writeString(path, new com.google.gson.GsonBuilder().create().toJson(result) + "\n");
@@ -73,7 +78,6 @@ public final class AerialRenderer24 {
var destination = connected == null ? clipped : connected;
var b = new Builder(s, destination, connected == null ? clip : s.bounds());
switch (s.kind()) {
case BALLOON -> b.balloon();
case MERCHANT_PROVISIONS, MERCHANT_TOOLS -> b.ship();
case MINERAL_ISLET -> b.islet();
}
@@ -114,99 +118,16 @@ public final class AerialRenderer24 {
set(x, y, z, state.setValue(BlockStateProperties.BED_PART, BedPart.FOOT));
set(x, y, z + 1, state.setValue(BlockStateProperties.BED_PART, BedPart.HEAD));
}
void balloon() {
int cx = s.width() / 2, cz = s.depth() / 2;
// The envelope is a rounded shell. Continuous spars attach the four real suspensions.
Block accent = (s.seed() & 1) == 0 ? Blocks.WOOL.yellow() : Blocks.WOOL.lightBlue();
for (int y = 9; y <= 27; y++) for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
double q = Math.pow((x - cx) / 7.1, 2) + Math.pow((z - cz) / 9.1, 2) + Math.pow((y - 18) / 9.2, 2);
if (q <= 1.02 && q >= .73) set(x, y, z, Math.abs(x - cx) <= 1 || y == 19 ? accent : y % 6 == 0 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
}
box(cx - 3, 0, cz - 4, cx + 3, 1, cz + 4, Blocks.OAK_PLANKS);
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 4; z <= cz + 4; z++) {
if (Math.abs(x - cx) == 3 || Math.abs(z - cz) == 4) {
set(x, 2, z, Blocks.OAK_PLANKS); slab(x, 3, z);
}
}
// One entry faces a usable standing deck, with a removable railing for boarding.
set(cx, 2, cz - 4, Blocks.OAK_FENCE_GATE); set(cx, 3, cz - 4, Blocks.AIR);
for (int x : new int[]{cx - 3, cx + 3}) for (int z : new int[]{cz - 4, cz + 4}) {
box(x, 3, z, x, 5, z, Blocks.STRIPPED_OAK_LOG);
box(x, 6, z, x, 12, z, Blocks.IRON_CHAIN);
}
for (int z : new int[]{cz - 4, cz + 4}) box(cx - 4, 12, z, cx + 4, 12, z, Blocks.STRIPPED_OAK_LOG);
for (int x : new int[]{cx - 3, cx + 3}) box(x, 12, cz - 5, x, 12, cz + 5, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 6, cz, cx + 3, 6, cz, Blocks.STRIPPED_OAK_LOG);
box(cx - 3, 5, cz - 4, cx - 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
box(cx + 3, 5, cz - 4, cx + 3, 6, cz + 4, Blocks.STRIPPED_OAK_LOG);
set(cx, 5, cz, Blocks.COPPER_BULB.waxed().unaffected().defaultBlockState().setValue(BlockStateProperties.LIT, true));
lantern(cx - 2, 5, cz); lantern(cx + 2, 5, cz);
set(cx + 2, 2, cz + 2, Blocks.CRAFTING_TABLE);
set(cx - 2, 2, cz - 2, Blocks.BARREL);
set(cx + 2, 2, cz - 2, Blocks.HAY_BLOCK);
}
int halfWidth(int y, int z) {
int end = Math.min(z, s.depth() - 1 - z);
return Math.max(0, Math.min(5, Math.min(1 + end, 1 + y)));
}
void ship() {
int cx = 5;
for (int z = 0; z < s.depth(); z++) for (int y = 0; y <= 5; y++) {
int half = halfWidth(y, z);
for (int x = cx - half; x <= cx + half; x++) {
boolean hull = y == 0 || y == 5 || Math.abs(x - cx) == half || z < 2 || z >= 25;
set(x, y, z, hull ? (y == 0 ? Blocks.STRIPPED_OAK_LOG : Blocks.OAK_PLANKS) : Blocks.AIR);
}
// The complete upright vanilla ship, offset one block in X for its saved envelope.
for (var entry : NativeMerchantShip27.restored().blocks().entrySet()) {
var at = entry.getKey(); set(at.getX() + 1, at.getY(), at.getZ(), entry.getValue());
}
// Curved gunwales remain closed; the wide centre stays clear for boarding small boats.
for (int z = 1; z <= 25; z++) {
int half = halfWidth(5, z);
for (int side : new int[]{-1, 1}) {
int x = cx + side * half;
set(x, 6, z, Blocks.OAK_FENCE); // Connection properties are completed below.
if (z % 5 == 0) set(x, 6, z, Blocks.STRIPPED_OAK_LOG);
}
}
for (int x = 3; x <= 7; x++) { set(x, 6, 1, Blocks.OAK_FENCE); set(x, 6, 25, Blocks.OAK_FENCE); }
// Roof, walls, threshold and cabin circulation share one continuous frame.
box(2, 6, 17, 8, 9, 24, Blocks.OAK_PLANKS);
box(3, 6, 18, 7, 9, 23, Blocks.AIR);
box(2, 10, 17, 8, 10, 24, Blocks.OAK_PLANKS);
for (int z = 17; z <= 24; z++) {
stair(1, 10, z, Direction.EAST, Half.BOTTOM); stair(9, 10, z, Direction.WEST, Half.BOTTOM);
}
for (int x : new int[]{2, 8}) for (int z : new int[]{17, 24}) box(x, 6, z, x, 10, z, Blocks.STRIPPED_OAK_LOG);
for (int z = 19; z <= 21; z++) for (int y = 7; y <= 8; y++) { set(2, y, z, Blocks.GLASS); set(8, y, z, Blocks.GLASS); }
for (int x = 4; x <= 6; x++) set(x, 8, 24, Blocks.GLASS);
door(5, 6, 17, Direction.SOUTH);
bed(3, 6, 22);
set(7, 6, 19, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
set(3, 6, 19, Blocks.CRAFTING_TABLE); set(3, 6, 20, Blocks.BARREL);
lantern(5, 9, 20); lantern(5, 9, 23);
// Cale reached by an actual supported stair run; no disconnected decorative floor.
for (int step = 0; step < 4; step++) {
int z = 10 + step, y = 4 - step;
box(5, 1, z, 5, y, z, Blocks.OAK_PLANKS);
box(5, y + 1, z, 5, 6, z, Blocks.AIR);
stair(5, y, z, Direction.NORTH, Half.BOTTOM);
}
box(3, 1, 7, 7, 1, 20, Blocks.OAK_PLANKS);
set(3, 2, 17, Blocks.BARREL); set(7, 2, 17, Blocks.BARREL);
lantern(3, 4, 8); lantern(7, 4, 8);
Block cargo = s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.HAY_BLOCK : Blocks.IRON_BLOCK;
box(2, 6, 5, 3, 7, 7, cargo); box(7, 6, 5, 8, 6, 7, Blocks.BARREL);
box(5, 6, 4, 5, 16, 4, Blocks.STRIPPED_OAK_LOG);
for (int x = 1; x <= 9; x++)
set(x, 16, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
for (int x = 2; x <= 8; x++)
set(x, 8, 4, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
set(5, 17, 4, Blocks.LANTERN);
// Seven by seven sail around the continuous mast: yard above, boom below, clear deck beneath.
for (int y = 9; y <= 15; y++) for (int x = 2; x <= 8; x++)
if (x != cx) set(x, y, 4, y == 11 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white());
// Stable handrails are explicit, rather than depending on neighbouring chunk updates.
for (int z = 2; z <= 24; z++) for (int x : new int[]{0, 10}) if (halfWidth(5, z) == 5 && z % 5 != 0 && !(x == 0 && z == 12))
set(x, 6, z, Blocks.OAK_FENCE.defaultBlockState().setValue(BlockStateProperties.NORTH, true).setValue(BlockStateProperties.SOUTH, true));
// Reuse the native stern cabin, with its original floor, roof, staircase and treasure-cache position.
set(3, 4, 23, s.kind() == AerialPlan24.Kind.MERCHANT_PROVISIONS ? Blocks.COMPOSTER : Blocks.SMITHING_TABLE);
bed(3, 4, 21);
set(7, 4, 21, Blocks.CRAFTING_TABLE);
lantern(5, 6, 22);
}
void islet() {
for (int x = 0; x < s.width(); x++) for (int z = 0; z < s.depth(); z++) {
@@ -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();
@@ -17,7 +17,7 @@ public final class AerialStructure24 extends Structure {
private final String family;
public AerialStructure24(StructureSettings settings, String family) {
super(settings);
if (!Set.of("balloon", "merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
if (!Set.of("merchant_ship", "mineral_islet").contains(family)) throw new IllegalArgumentException("Unknown aerial family");
this.family = family;
}
public String family() { return family; }
@@ -22,12 +22,11 @@ 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 five 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> BALLOON = key("aerial_balloon");
public static final ResourceKey<Structure> MERCHANT_SHIP = key("aerial_merchant_ship");
public static final ResourceKey<Structure> MINERAL_ISLET = key("aerial_mineral_islet");
public static final List<ResourceKey<Structure>> KEYS = List.of(BALLOON, MERCHANT_SHIP, MINERAL_ISLET);
public static final List<ResourceKey<Structure>> KEYS = List.of(MERCHANT_SHIP, MINERAL_ISLET);
public static final StructureType<AerialStructure24> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
SanctuaryMod.id("aerial_v24"), () -> AerialStructure24.CODEC);
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
@@ -37,10 +36,10 @@ public final class AerialStructures24 {
private static ResourceKey<Structure> key(String id) { return ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id(id)); }
public static void register() {}
public static String family(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> "balloon"; case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
return switch (site.kind()) { case MINERAL_ISLET -> "mineral_islet"; default -> "merchant_ship"; };
}
public static ResourceKey<Structure> key(AerialPlan24.Site site) {
return switch (site.kind()) { case BALLOON -> BALLOON; case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
return switch (site.kind()) { case MINERAL_ISLET -> MINERAL_ISLET; default -> MERCHANT_SHIP; };
}
public static String structureId(AerialPlan24.Site site) { return key(site).identifier().toString(); }
public static boolean isPlacing() { return PLACING.get() != null; }
@@ -68,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);
}
}
@@ -0,0 +1,81 @@
package fr.koka.sanctuary.worldgen.aerial;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
/** The actual Mojang upright ship, with local repairs; the vanilla hull, cabin, stairs and slabs remain the source. */
public final class NativeMerchantShip27 {
public static final String TEMPLATE = "minecraft:shipwreck/rightsideup_full";
public static final int WIDTH = 9, DEPTH = 28, NATIVE_HEIGHT = 9, PALETTE = 0;
private NativeMerchantShip27() {}
public record Restored(Map<BlockPos, BlockState> blocks, int nativeBlocks, int preservedBlocks,
int addedBlocks, int replacedBlocks, int drainedBlocks) {}
private static final class Loaded { private static final Restored VALUE = load(); }
public static Restored restored() { return Loaded.VALUE; }
private static Restored load() {
String resource = "/data/minecraft/structure/shipwreck/rightsideup_full.nbt";
try (var input = NativeMerchantShip27.class.getResourceAsStream(resource)) {
if (input == null) throw new IOException("Missing vanilla ship template " + resource);
var tag = NbtIo.readCompressed(input, NbtAccounter.defaultQuota());
var size = tag.getListOrEmpty("size");
if (size.getIntOr(0, -1) != WIDTH || size.getIntOr(1, -1) != NATIVE_HEIGHT || size.getIntOr(2, -1) != DEPTH)
throw new IOException("Unexpected vanilla ship dimensions");
var palettes = tag.getListOrEmpty("palettes");
var palette = palettes.getListOrEmpty(PALETTE);
var states = new java.util.ArrayList<BlockState>();
for (var value : palette) states.add(BlockState.CODEC.parse(NbtOps.INSTANCE, value).getOrThrow());
var original = new LinkedHashMap<BlockPos, BlockState>();
for (var value : tag.getListOrEmpty("blocks")) {
var block = value.asCompound().orElseThrow(); var pos = block.getListOrEmpty("pos");
var at = new BlockPos(pos.getIntOr(0, -1), pos.getIntOr(1, -1), pos.getIntOr(2, -1));
var state = states.get(block.getIntOr("state", -1));
if (!state.is(Blocks.STRUCTURE_BLOCK)) original.put(at, state);
}
var result = new LinkedHashMap<>(original);
int drained = (int) original.values().stream().filter(state -> state.hasProperty(BlockStateProperties.WATERLOGGED)
&& state.getValue(BlockStateProperties.WATERLOGGED)).count();
result.replaceAll((at, state) -> state.hasProperty(BlockStateProperties.WATERLOGGED)
? state.setValue(BlockStateProperties.WATERLOGGED, false) : state);
// Keep only the treasure chest as the site's loot chest; other native caches become empty barrels.
for (var entry : original.entrySet()) if (entry.getValue().is(Blocks.CHEST))
result.put(entry.getKey(), Blocks.BARREL.defaultBlockState());
result.put(new BlockPos(6, 4, 24), Blocks.CHEST.defaultBlockState());
// Repair missing side planks by continuing their actual neighbouring native row.
for (int z : new int[]{12, 15, 18}) for (int x : new int[]{1, 2, 6, 7}) {
var at = new BlockPos(x, 3, z);
var neighbour = new BlockPos(x, 3, z - 1);
if (!result.containsKey(at) && original.containsKey(neighbour)) result.put(at, original.get(neighbour));
}
// Restore the existing stern cabin's two damaged window strips, leaving its native stepped roof intact.
for (int x : new int[]{1, 7}) for (int z = 21; z <= 24; z++)
for (int y = 5; y <= 6; y++) result.put(new BlockPos(x, y, z), Blocks.GLASS.defaultBlockState());
for (int x : new int[]{0, 8}) result.put(new BlockPos(x, 6, 22), Blocks.OAK_PLANKS.defaultBlockState());
// The original main mast starts at (4,2,11); extend it rather than inventing another hull.
for (int y = 6; y <= 16; y++) result.put(new BlockPos(4, y, 11), Blocks.OAK_LOG.defaultBlockState());
var yard = Blocks.OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X);
for (int x = 0; x < WIDTH; x++) result.put(new BlockPos(x, 16, 11), yard);
for (int y = 10; y <= 15; y++) for (int x = 1; x <= 7; x++) if (x != 4)
result.put(new BlockPos(x, y, 11), (y == 12 ? Blocks.WOOL.lightGray() : Blocks.WOOL.white()).defaultBlockState());
// Complete the short rear cargo hoist using its native stump, clear of the cabin entrance.
for (int y = 6; y <= 8; y++) result.put(new BlockPos(4, y, 16), Blocks.OAK_LOG.defaultBlockState());
for (int x = 3; x <= 5; x++) result.put(new BlockPos(x, 8, 16), yard);
int preserved = 0, added = 0, replaced = 0;
for (var entry : result.entrySet()) {
var old = original.get(entry.getKey());
if (old == null) added++; else if (old.equals(entry.getValue())) preserved++; else replaced++;
}
return new Restored(Map.copyOf(result), original.size(), preserved, added, replaced, drained);
} catch (IOException | RuntimeException error) {
throw new IllegalStateException("Cannot restore required vanilla ship " + TEMPLATE, error);
}
}
}
@@ -226,7 +226,7 @@
"sanctuary.structures.census.status.incompatible": "Incompatible habitat",
"sanctuary.structures.census.status.other_dimension": "Reserved for another dimension",
"sanctuary.mineral_sanctuary.entry": "Mineral sanctuary: %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes and five aerial sites. Vanilla structures remain independent.",
"sanctuary.world_options.aerial24_hint": "Secrets, abandoned routes, two ships and two floating islets. Vanilla structures remain independent.",
"sanctuary.structures.census.aerial.none": "No aerial sites reserved in this world.",
"sanctuary.structures.census.aerial.unknown_site": "Unknown aerial site: %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · starts %s/%s · features %s/%s · full chunks %s/%s",
@@ -235,7 +235,7 @@
"sanctuary.structures.census.aerial.status.generated": "generated",
"sanctuary.structures.census.aerial.status.missing": "start missing after verification",
"sanctuary.structures.census.aerial.status.unknown": "unknown",
"structure.sanctuary.aerial_balloon": "Hot air balloon",
"structure.sanctuary.aerial_merchant_ship": "Merchant ship",
"structure.sanctuary.aerial_mineral_islet": "Mineral islet"
"structure.sanctuary.aerial_mineral_islet": "Mineral islet",
"biome.sanctuary.swamp27_caves": "Underground swamp"
}
@@ -226,7 +226,7 @@
"sanctuary.structures.census.status.incompatible": "Milieu incompatible",
"sanctuary.structures.census.status.other_dimension": "Réservée à une autre dimension",
"sanctuary.mineral_sanctuary.entry": "Sanctuaire minéral : %s %s %s",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées et cinq lieux suspendus. Les structures vanilla restent indépendantes.",
"sanctuary.world_options.aerial24_hint": "Secrets, voies abandonnées, deux navires et deux îlots suspendus. Les structures vanilla restent indépendantes.",
"sanctuary.structures.census.aerial.none": "Aucun lieu suspendu réservé dans ce monde.",
"sanctuary.structures.census.aerial.unknown_site": "Lieu suspendu inconnu : %s",
"sanctuary.structures.census.aerial.site": "%s · %s · %s · départs %s/%s · décoration %s/%s · chunks complets %s/%s",
@@ -235,7 +235,7 @@
"sanctuary.structures.census.aerial.status.generated": "généré",
"sanctuary.structures.census.aerial.status.missing": "départ absent après vérification",
"sanctuary.structures.census.aerial.status.unknown": "inconnu",
"structure.sanctuary.aerial_balloon": "Montgolfière",
"structure.sanctuary.aerial_merchant_ship": "Bateau marchand",
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral"
"structure.sanctuary.aerial_mineral_islet": "Îlot minéral",
"biome.sanctuary.swamp27_caves": "Marais souterrain"
}
@@ -1,127 +0,0 @@
{
"type": "minecraft:chest",
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:spyglass"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:compass"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:clock"
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:gold_ingot",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:diamond",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 2,
"max": 4
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:lead",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 4,
"max": 8
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:bread",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 12,
"max": 24
}
}
]
}
]
},
{
"rolls": 1,
"entries": [
{
"type": "minecraft:item",
"name": "minecraft:emerald",
"functions": [
{
"function": "minecraft:set_count",
"count": {
"type": "minecraft:uniform",
"min": 8,
"max": 16
}
}
]
}
]
}
]
}
@@ -0,0 +1,172 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.swamp"
}
},
"minecraft:gameplay/increased_fire_burnout": true,
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:sheep",
"count": 4,
"weight": 12
},
{
"type": "minecraft:pig",
"count": 4,
"weight": 10
},
{
"type": "minecraft:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:cow",
"count": 4,
"weight": 8
},
{
"type": "minecraft:frog",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 5,
"min_inclusive": 2
},
"weight": 10
}
],
"monster": [
{
"type": "minecraft:spider",
"count": 4,
"weight": 100
},
{
"type": "minecraft:zombie",
"count": 4,
"weight": 95
},
{
"type": "minecraft:zombie_villager",
"count": 1,
"weight": 5
},
{
"type": "minecraft:skeleton",
"count": 4,
"weight": 70
},
{
"type": "minecraft:creeper",
"count": 4,
"weight": 100
},
{
"type": "minecraft:slime",
"count": 4,
"weight": 100
},
{
"type": "minecraft:enderman",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 1
},
"weight": 10
},
{
"type": "minecraft:witch",
"count": 1,
"weight": 5
},
{
"type": "minecraft:slime",
"count": 1,
"weight": 1
},
{
"type": "minecraft:bogged",
"count": 4,
"weight": 30
}
],
"underground_water_creature": [
{
"type": "minecraft:glow_squid",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 4
},
"weight": 10
}
]
}
},
"modifier": "overlay"
},
"minecraft:visual/sky_color": "#78a7ff",
"minecraft:visual/water_fog_color": "#232317",
"minecraft:visual/water_fog_end_distance": {
"argument": 0.85,
"modifier": "multiply"
}
},
"carvers": [],
"downfall": 0.9,
"effects": {
"dry_foliage_color": "#7b5334",
"foliage_color": "#6a7039",
"grass_color_modifier": "swamp",
"water_color": "#617b64"
},
"features": [
[],
[],
[],
[],
[],
[],
[
"sanctuary:rift_ore_coal",
"sanctuary:rift_ore_iron",
"sanctuary:rift_ore_copper",
"sanctuary:rift_ore_gold",
"sanctuary:rift_ore_redstone",
"sanctuary:rift_ore_lapis",
"sanctuary:rift_ore_diamond",
"sanctuary:rift_ore_emerald",
"sanctuary:rift_ore_coal_surface",
"sanctuary:rift_ore_iron_surface",
"sanctuary:rift_ore_copper_surface",
"sanctuary:rift_ore_coal_ledges",
"sanctuary:rift_ore_iron_ledges",
"sanctuary:rift_ore_copper_ledges"
],
[],
[],
[
"minecraft:glow_lichen",
"sanctuary:swamp27_decoration"
],
[]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -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"
}
@@ -49,5 +49,19 @@
"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
}
}
@@ -49,5 +49,19 @@
"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
}
}
@@ -49,5 +49,19 @@
"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
}
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:swamp27_decoration"
},
"placement": []
}
@@ -1,10 +0,0 @@
{
"type": "sanctuary:aerial_v24",
"family": "balloon",
"biomes": [
"minecraft:the_void"
],
"step": "top_layer_modification",
"terrain_adaptation": "none",
"spawn_overrides": {}
}
@@ -51,6 +51,7 @@
"sanctuary:population_lush_caves",
"sanctuary:population_dripstone_caves",
"sanctuary:lost_city",
"sanctuary:swamp27_caves",
"minecraft:ocean",
"minecraft:deep_ocean",
"minecraft:cold_ocean",
@@ -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,34 @@
package fr.koka.sanctuary.worldgen;
/** Small pure selector test; it does not claim that biome volumes contain generated air. */
public final class Swamp27VolumesSmoke {
public static void main(String[] args) {
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 42, 2026}) {
int selected = 0, total = 0, stackedChanges = 0;
for (int x = -diameter / 3; x <= diameter / 3; x += 16)
for (int z = -diameter / 3; z <= diameter / 3; z += 16) {
boolean below = Swamp27Volumes.contains(seed, diameter, x, 100, z);
if (below != Swamp27Volumes.contains(seed, diameter, x, 164, z)) stackedChanges++;
for (int y = 88; y <= 172; y += 12) {
boolean first = Swamp27Volumes.contains(seed, diameter, x, y, z);
// Other probes must not alter the same result.
Swamp27Volumes.contains(seed ^ 87, diameter, -x, 256, -z);
require(first == Swamp27Volumes.contains(seed, diameter, x, y, z), "mutable selector");
if (first) selected++;
total++;
}
for (int y : new int[] {-64, 0, 71, 188, 200, 256, 383, 384})
require(!Swamp27Volumes.contains(seed, diameter, x, y, z), "surface/out-of-height selected");
}
require(selected > 0 && selected < total, "sector must coexist with other caves");
require(stackedChanges > 0, "biome field became a full-height column");
require(!Swamp27Volumes.contains(seed, diameter, diameter, 120, 0), "outside island selected");
System.out.println("swamp27 " + diameter + "/" + seed + " selected=" + selected
+ "/" + total + " vertical_changes=" + stackedChanges);
}
require(!Swamp27Volumes.contains(0, 0, 0, 120, 0), "invalid diameter");
}
private static void require(boolean value, String message) {
if (!value) throw new AssertionError(message);
}
}
@@ -19,7 +19,7 @@ import net.minecraft.world.level.storage.loot.LootTable;
/** Pure finite planning witnesses: nine size/seed combinations, rotation and pre-visit claims. */
public final class Aerial24Smoke {
public static void main(String[] args) {
public static void main(String[] args) throws java.io.IOException {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
BlockState[] column = new BlockState[384];
for (int y = 0; y < column.length; y++) column[y] = (y >= 80 && y <= 240 ? Blocks.STONE : Blocks.AIR).defaultBlockState();
@@ -27,10 +27,13 @@ public final class Aerial24Smoke {
int[] queries = {0};
var plan = AerialPlanner24.plan(seed, size, (x, z) -> { queries[0]++; return new NoiseColumn(0, column); });
var replay = AerialPlanner24.plan(seed, size, (x, z) -> new NoiseColumn(0, column));
String preview = System.getProperty("sanctuary.aerial.preview");
if (preview != null && size == 724 && seed == 42)
AerialRenderer24.writeDiagnostic(java.nio.file.Path.of(preview), plan.sites().stream().filter(AerialPlan24.Site::merchant).toList());
require(plan.sites().equals(replay.sites()) && plan.claimsChunks().equals(replay.claimsChunks()), "Plan does not depend on time or loaded chunks");
require(queries[0] <= AerialPlanner24.MAX_COLUMNS, "Finite terrain query budget");
require(plan.sites().size() == 5 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Five complete sites, including two distinct merchant ships");
require(plan.sites().size() == 4 && plan.sites().stream().filter(AerialPlan24.Site::merchant).count() == 2,
"Four complete sites, including two distinct merchant ships");
require(plan.sites().stream().filter(AerialPlan24.Site::waterfall).count() == 1, "Exactly one waterfall site");
var allClaims = new HashSet<Long>();
var root = ExpansionIsland.origin(size, seed, 24);
@@ -45,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 1648 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);
@@ -68,7 +83,7 @@ public final class Aerial24Smoke {
require(AerialPiece24.decode(AerialPiece24.encode(site)).equals(site), "Saved site descriptor round-trips without changing its seed or rotation");
if (seed == 0) verifyRender(site);
}
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the five finite site masks");
require(allClaims.equals(plan.claimsChunks()), "Combined claims contain only the four finite site masks");
var far = new ExpansionIsland("far", "sanctuary", "east", "temperate", 64, 4096, 4096, 1, "natural", "reserved", 24);
require(!plan.conflicts(far), "Aerial sites do not reserve an infinite ring around Sanctuary");
require(!plan.conflicts(root), "Root ownership is not treated as a future expansion");
@@ -78,11 +93,11 @@ public final class Aerial24Smoke {
catch (UnsupportedOperationException expected) { }
}
require(AerialPlan24.EMPTY.isEmpty() && AerialPlan24.EMPTY.claimsChunks().isEmpty(), "Disabled sites reserve no space");
try { new AerialPlan24(List.of(new AerialPlan24.Site("balloon", AerialPlan24.Kind.BALLOON,
500, 290, 0, 15, 28, 19, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
try { new AerialPlan24(List.of(new AerialPlan24.Site("merchant_provisions", AerialPlan24.Kind.MERCHANT_PROVISIONS,
500, 290, 0, 11, 18, 28, 0, 0, 250, 240, 0, false))); throw new AssertionError("Partial plan accepted"); }
catch (IllegalArgumentException expected) { }
verifyReservedShoreAndView(column);
System.out.println("Aerial24Smoke: nine deterministic plans, five sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
System.out.println("Aerial24Smoke: nine deterministic plans, four sites, four rotations, finite non-overlapping claims and pre-visit conflicts passed.");
}
private static void verifyReservedShoreAndView(BlockState[] column) {
var original = AerialPlanner24.plan(0, 724, (x, z) -> new NoiseColumn(0, column)).sites().getFirst();
@@ -106,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) {
@@ -126,6 +141,24 @@ public final class Aerial24Smoke {
require(complete.equals(clipped) && clippedChests[0] == 1, "Chunk clipping reconstructs the complete authored structure without duplicate chests");
long sources = complete.values().stream().filter(state -> state.is(Blocks.WATER)).count();
require(sources == (site.waterfall() ? 1 : 0), "Renderer writes one source, never a fabricated full waterfall column");
if (site.merchant()) {
require(complete.values().stream().allMatch(state -> state.getFluidState().isEmpty()),
"Restored ships contain no fluid, including inside stairs, slabs, fences or trapdoors");
var nativeShip = NativeMerchantShip27.restored();
require(nativeShip.nativeBlocks() == 659, "The exact vanilla template contributes 662 authored blocks minus its three data markers");
require(nativeShip.preservedBlocks() + nativeShip.replacedBlocks() == nativeShip.nativeBlocks(), "No native hull position disappears during restoration");
require(nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.OAK_STAIRS))
&& nativeShip.blocks().values().stream().anyMatch(state -> state.is(Blocks.SPRUCE_SLAB)),
"The ship retains its native shaped stairs and slabs");
require(complete.values().stream().noneMatch(state -> state.is(Blocks.STRUCTURE_BLOCK)), "No native data marker is placed in the restored ship");
for (var at : AerialGeometry24.interiorChecks(site)) {
require(complete.getOrDefault(at, Blocks.AIR.defaultBlockState()).isAir()
&& complete.getOrDefault(at.above(), Blocks.AIR.defaultBlockState()).isAir(),
"Boarding, cabin circulation and merchant standing spaces remain open");
require(complete.getOrDefault(at.below(), Blocks.AIR.defaultBlockState()).isSolid(),
"Each boarding and interior witness retains its real floor");
}
}
if (site.kind() == AerialPlan24.Kind.MINERAL_ISLET) {
var chest = AerialGeometry24.chest(site);
for (int dy = 1; dy <= 3; dy++) require(complete.get(chest.above(dy)) != null && complete.get(chest.above(dy)).isSolid(), "Mineral chest is buried under three intact layers");
@@ -48,9 +48,12 @@ public final class AerialPersistence24Smoke {
require(!site.ownsChunk((site.maxX() >> 4) + 2, (site.maxZ() >> 4) + 2), "Unused corners of the old broad margin stay free");
}
require(!Files.exists(directory), "Manifest creation must not prematurely initialize the journal");
require(draws.get() == 1 && stored.plan().sites().size() == 5, "One complete initial plan is recorded");
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,
@@ -65,7 +68,7 @@ public final class AerialPersistence24Smoke {
journal.commit(List.of(root));
require(Arrays.equals(pristineJournal, Files.readAllBytes(journalFile)), "A no-op commit preserves bytes");
var collision = child("collision", 672, 0);
var collision = child("collision", "south", 0, 672);
require(!collision.conflicts(root), "Collision witness is outside initial island reservations");
require(stored.plan().conflicts(collision), "A never-generated satellite reserves its chunks");
reject(() -> journal.commit(List.of(root, collision)), "Reservation over an unvisited aerial site");
@@ -135,17 +138,16 @@ public final class AerialPersistence24Smoke {
}
}
private static ExpansionIsland child(String id, int x, int z) {
return new ExpansionIsland(id, "sanctuary", "east", "temperate", 64, x, z,
private static ExpansionIsland child(String id, String direction, int x, int z) {
return new ExpansionIsland(id, "sanctuary", direction, "temperate", 64, x, z,
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", 24);
}
private static AerialPlan24 fixture() {
return new AerialPlan24(List.of(
new Site("balloon", Kind.BALLOON, 660, 290, 0, 15, 28, 19, 0, 1, 390, 240, 0, false),
new Site("merchant_provisions", Kind.MERCHANT_PROVISIONS, 0, 235, 660, 11, 18, 27, 0, 2, 0, 240, 390, false),
new Site("merchant_tools", Kind.MERCHANT_TOOLS, -740, 242, 0, 11, 18, 27, 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("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, -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); }
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.24"
version = "0.1.0-alpha.28"
pack-format = "packwiz:1.1.0"
[index]