Add living caverns and connected water terraces for alpha.9
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 02:18:35 +02:00
parent bf84b37ea7
commit 40b655517e
60 changed files with 8579 additions and 48 deletions
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## 0.1.0-alpha.9 — 2026-09-09
- Cavités luxuriantes éclairées par leur végétation, secteurs à spéléothèmes
et géodes daméthyste recherchés sous l’île, sur des supports naturels.
- Province de soufre limitée dans les profondeurs ; geysers utilisant
les blocs et conditions de fonctionnement de Minecraft 26.3-pre-2.
- Forêts et rivière de surface conservées ; recherche de lacs plus profonds
et volumineux et de groupes facultatifs de bassins en terrasses reliés par
des cascades. Les « rizières » désignent le relief aquatique, sans culture
de riz ni mécanique dascenseur ajoutée au joueur.
- Nouvelle clé `sanctuary:sanctuary_cavern`, réservée aux nouveaux mondes.
Les générations jusqu’à lalpha.8 restent séparées ; aucune sauvegarde
existante nest convertie ou régénérée.
Validation : au moins dix tests requis réussis sur chacune des graines `0`,
`42` et `8675309`, avec 1 800 ticks de fluides ; `check build assemblePack`
réussi. Les preuves cumulées comprennent deux bassins en terrasses reliés,
un geyser dormant puis éruptif, des baies lumineuses, des spéléothèmes et de
laméthyste bourgeonnante. Les mesures et leurs périmètres figurent dans
[Validation](docs/testing.md). Publication et synchronisations Prism restent
suivies séparément. Aucun site ni stock nest garanti sur chaque graine.
## 0.1.0-alpha.8 — 2026-09-09
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
+31 -11
View File
@@ -17,7 +17,7 @@ conserve les intentions ; le [backlog](docs/backlog.md) prépare les premiers
tickets. Les systèmes d'expansion, d'économie, de progression et les dimensions
décrits dans la vision ne sont pas encore implémentés.
## Premier incrément
## Socle livré jusqu’à lalpha.8
- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz.
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
@@ -57,14 +57,34 @@ le terrain : le spawn cherche un emplacement naturel. Le catalogue
TerraMix, les continents déverrouillables, les océans, les ruines et
les autres fonctionnalités restent à construire par tickets.
Lalpha.8 utilise la nouvelle génération `sanctuary:sanctuary_rift` ; créer un
**nouveau monde** pour lessayer. Les anciennes générations, jusqu’à lalpha.7,
gardent leurs paramètres ; aucune sauvegarde nest régénérée. Dappled Forest
reste réservé aux futurs continents et présent dans les anciennes sauvegardes
alpha.5. **La validation moteur de lalpha.8 passe sur les trois graines de
référence**, avec 1 800 ticks de fluides ; aucune rivière, coulée ou quantité
de ressources nest garantie pour toutes les graines. Les observations et
leurs limites figurent dans [Validation](docs/testing.md).
La génération alpha.8, `sanctuary:sanctuary_rift`, passe les contrôles moteur
sur les trois graines de référence, avec 1 800 ticks de fluides. Ces résultats
ne valident pas par avance lincrément suivant. Les observations et leurs
limites figurent dans [Validation](docs/testing.md).
## Alpha.9 : cavités vivantes et terrasses deau
Lincrément ajoute des cavités luxuriantes et lumineuses sous l’île, des
secteurs à spéléothèmes, des géodes daméthyste et des poches de soufre moins
envahissantes, avec de vrais geysers de soufre lorsque leurs conditions sont
réunies. Les forêts de surface sont conservées ; la recherche de lacs plus
profonds et volumineux accompagne des groupes facultatifs de bassins naturels
étagés. Leurs cascades relient les paliers et permettent de circuler par la nage.
Limage des « rizières » décrit ce paysage deau en terrasses : aucune culture
de riz nest ajoutée.
**La validation moteur alpha.9 passe sur les trois graines de référence.**
Les preuves de terrasses, geysers et décor souterrain sont cumulées sur ces
trois mondes, sans imposer chaque élément à chacun. La distribution est
vérifiée séparément, selon le guide packwiz. Le
nouveau réglage `sanctuary:sanctuary_cavern` nécessite de créer un **nouveau
monde Sanctuary**. Les anciennes générations jusqu’à lalpha.8 gardent leurs
paramètres ; aucune sauvegarde nest régénérée. Les cavités restent dans
lOverworld, sans créer la dimension des cavernes prévue par la vision.
Dappled Forest reste réservé aux futurs continents et présent dans les
anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
de ressources nest garanti sur chaque graine. Voir le ticket local
[WG-10](docs/backlog.md).
Le premier champ de recherche climatique est défini : nord froid, sud chaud,
ouest sec et est humide, avec leurs combinaisons diagonales. Un outil de
@@ -88,7 +108,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.8 |
| Sanctuary / pack | 0.1.0-alpha.9 |
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
@@ -115,7 +135,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.8.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.9.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).
+54
View File
@@ -13,6 +13,7 @@ visuels, multijoueurs et de redémarrage indiqués dans ce document restent à
faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent ouverts,
avec les incréments de l’île initiale décrits ci-dessous. WG-08 est validé ;
WG-09 est livré en alpha.8, avec mesures moteur et mise à jour Prism vérifiées.
WG-10 dispose de mesures moteur alpha.9 ; la distribution reste suivie séparément.
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
@@ -254,6 +255,59 @@ est synchronisée deux fois, avec sauvegardes et réglages conservés. Le ticket
est livré ; lappréciation visuelle et l’équilibrage en partie restent à
éprouver. Ce ticket local nest pas une issue distante publiée.
### WG-10 — Cavités luxuriantes et terrasses deau — alpha.9
**Branche :** `codex/lush-caverns-and-water-terraces`.
**But :** rendre les cavités sous Sanctuary plus vivantes et lumineuses,
diversifier leurs ressources et créer des eaux plus volumineuses, sans
remplacer les forêts et la rivière de surface appréciées en alpha.8.
**Critères dacceptation :**
- Des cavités luxuriantes portent une végétation réellement lumineuse et
enracinée dans les surfaces et plafonds existants. Les observations doivent
distinguer un biome déclaré, ses blocs décoratifs et leur lumière réelle.
- Des secteurs à spéléothèmes et des géodes daméthyste sont recherchés dans
la roche compatible ; leurs supports, leurs volumes et leur accessibilité
sont contrôlés dans les vrais chunks, sans géodes suspendues dans le vide.
- Le soufre occupe moins de roche quen alpha.8, sur un périmètre de comparaison
explicite. Les geysers éventuels utilisent le soufre actif et les conditions
exactes de Minecraft 26.3-pre-2 ; leur fonctionnement doit être démontré,
sans confondre un bloc jaune avec un geyser actif.
- Les forêts, la rivière et les bassins de surface restent présents lorsque
le relief le permet. Les lacs plus profonds et volumineux sont mesurés en
blocs deau réels, avec fond et parois retenus après simulation.
- Des groupes facultatifs de deux bassins naturels en terrasses
peuvent relier plusieurs niveaux par des cascades déclarées. Chaque palier
doit avoir ses supports et son propre volume retenu ; une cascade isolée
ou des plans deau sans liaison ne valident pas un groupe de terrasses.
Le second bassin peut avoir une sortie terminale vers les roches ou le vide,
sans créer un troisième palier.
- Ces cascades offrent un passage vertical selon les règles de nage de
Minecraft. « Rizières » désigne seulement leur disposition paysagère :
aucune culture de riz ni téléportation nentre dans ce ticket.
- Les sorties deau sont contrôlées après décoration et après 1 800 ticks,
y compris aux frontières de chunks. Leur finition initiale doit préserver
les aménagements du joueur aux rechargements suivants.
- Le nouveau réglage `sanctuary:sanctuary_cavern` sapplique aux nouveaux
mondes. Les identifiants et traitements alpha.1 à alpha.8 restent séparés ;
aucune sauvegarde personnelle nest modifiée par les essais.
- Les graines `0`, `42` et `8675309` disposent de mesures identifiées par
version. Les sites absents sont signalés ; aucun quota de ressources ni
bassin de secours ne remplit un relief incompatible.
- Après validation : `check build assemblePack`, artefacts immuables vérifiés,
deux synchronisations isolées puis deux dans la même instance Prism,
sauvegardes et réglages conservés.
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
passent les contrôles finaux. Les preuves cumulées démontrent un groupe de
deux paliers reliés, un geyser actif et les décors souterrains demandés ; les
absences locales et périmètres de mesure figurent dans [Validation](testing.md).
Le build est réussi. La publication et la synchronisation de la même instance
Prism restent vérifiées lors de la distribution. Ce ticket local nest pas
une issue distante publiée ; lessai visuel et l’équilibrage restent ouverts.
## Réserve de thèmes futurs
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
+14 -2
View File
@@ -15,6 +15,11 @@ de l'ancien pack 26.2.
## Livrer une mise à jour
Lalpha.9 est en préparation dans les sources. Les exemples de commandes
ci-dessous ciblent cette prochaine version ; ils ne signifient pas quelle
est publiée. Le canal reste sur la dernière livraison vérifiée jusqu’à la
validation du build, des essais moteur et des nouveaux artefacts.
Dernière livraison vérifiée le 9 septembre 2026 :
[Sanctuary 0.1.0-alpha.8](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.8),
depuis le commit source `80e78e0`. Le JAR, le `.mrpack` et le ZIP damorçage
@@ -36,7 +41,7 @@ Après les vérifications, le commit et le push de la branche du ticket, le scri
de publication réalise les étapes de release et de canal ci-dessous :
```sh
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.8.mrpack
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.9.mrpack
```
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
@@ -55,7 +60,7 @@ La procédure complète, également utilisable manuellement :
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
```sh
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.8/sanctuary-0.1.0-alpha.8.jar
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.9/sanctuary-0.1.0-alpha.9.jar
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
@@ -154,6 +159,13 @@ nouveau monde Sanctuary pour les découvrir. La même instance Prism reçoit
le nouveau JAR par le canal stable ; ses sauvegardes et réglages sont conservés.
Les mondes alpha.7 et antérieurs gardent leurs paramètres de génération.
Lalpha.9 prépare `sanctuary:sanctuary_cavern` pour les cavités luxuriantes,
les géodes, les geysers et les terrasses deau. Un **nouveau monde Sanctuary**
sera nécessaire pour essayer cet incrément après sa validation. Les mondes
alpha.8 conservent `sanctuary:sanctuary_rift` ; leur contenu nest pas retouché
par une mise à jour de la même instance Prism. Les anciennes sauvegardes et
les réglages restent à conserver lors des deux synchronisations de livraison.
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
+167 -1
View File
@@ -65,7 +65,8 @@ Le test capture donc le spawn juste avant ce déplacement, après l'initialisati
normale du monde, et examine les coordonnées absolues de l'île.
Les scénarios suivants couvrent le socle et les générations historiques ;
les contrôles et résultats propres à lalpha.8 figurent plus bas :
les contrôles validés pour lalpha.9 et les résultats historiques
propres à lalpha.8 figurent plus bas :
1. Le vrai générateur Sanctuary est chargé et le spawn collectif repose sur une
surface déjà présente dans la densité naturelle, avec un sol plein de 3×3
@@ -189,6 +190,171 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
## Contrôles et validation alpha.9 — 9 septembre 2026
Les mondes neufs des graines `0`, `42` et `8675309` utilisent les paramètres
de production `sanctuary:sanctuary_cavern`. Chaque journal termine au moins
dix tests requis et sa tâche Gradle sans échec, avec 1 800 ticks de fluides.
Les sources naturelles deau et de lave, les débordements et les geysers
démarrent uniquement avec leurs ticks de production. Le test programme
seulement un témoin artificiel indépendant pour vérifier que les ticks
fonctionnent ; il ne réveille pas les sources naturelles.
Le build complet `check build assemblePack` réussit en 6m 24s,
avec 15 tâches de tests purs de comportement.
Les rapports sont postérieurs au gel du code final ; les résultats de
lalpha.8 et les essais alpha.9 intermédiaires ne servent pas de preuve ici.
La présence dun groupe de terrasses connecté, dun geyser réellement actif,
de baies lumineuses, de spéléothèmes et daméthyste bourgeonnante est vérifiée
sur lensemble des trois graines. Chaque élément nest pas imposé à chaque
graine ; leurs absences locales restent visibles dans les tableaux.
| Graine | Chunks FULL | Failles / témoins | Chênes / bouleaux supérieurs | Chênes noirs inférieurs | Gros champignons | Petits sous plafond | Minerais dehors : charbon / fer / cuivre |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 251 | 3 / 3 | 321 / 251 | 18 | 14 | 211 | 506 / 178 / 147 |
| `42` | 246 | 2 / 2 | 362 / 177 | 11 | 8 | 69 | 617 / 269 / 193 |
| `8675309` | 242 | 3 / 3 | 248 / 125 | 14 | 11 | 131 | 761 / 317 / 328 |
Les arbres sont des groupes de pieds de troncs sur sol naturel, avec feuilles
observées ; les gros champignons ont une tige et un chapeau connectés.
| Graine | Blocs à baies lumineuses | Blocs émissifs | Spéléothèmes pointus | Améthyste bourgeonnante | Blocs daméthyste | Geysers dormants + éruptifs | Sources deau de geyser |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 363 | 391 | 501 | 214 | 818 | 1 | 1 |
| `42` | 280 | 308 | 592 | 210 | 945 | 4 | 4 |
| `8675309` | 446 | 476 | 380 | 141 | 504 | 5 | 5 |
Le décor souterrain est compté sur les blocs initiaux réellement générés.
Ses témoins sont revérifiés après les fluides : support, conservation de
laméthyste bourgeonnante et lumière du moteur pour les blocs émissifs.
Ces nombres comptent des blocs, pas des cavités ni des géodes distinctes.
Laméthyste bourgeonnante est reliée à une cavité naturelle par un parcours
dair vérifié avant et après les ticks ; les JSON donnent la position de
chaque raccord et les exemples de blocs souterrains, limités à 64 témoins.
Les geysers observés possèdent la vraie entité de bloc `potent_sulfur`,
du magma dessous et une source deau retenue au-dessus. Un même geyser
doit passer par les états dormant et éruptif pendant les ticks, sans
activation artificielle par le test.
| Graine | Niveaux Y des bassins | Colonnes deau par bassin | Descente depuis la bouche | Nouveaux blocs écoulés accessibles | Deux paliers connectés |
| --- | --- | --- | --- | --- | --- |
| `0` | 148 / 140 | 53 / 76 | 148 | 1 230 | Oui, après ticks |
| `0` | 177 / 163 | 169 / 47 | 177 | 3 370 | Oui, après ticks |
| `42` | 129 / 114 | 116 / 53 | 129 | 181 | Oui, après ticks |
| `42` | 112 / 107 | 105 / 161 | 112 | 354 | Oui, après ticks |
| `8675309` | 154 / 149 | 128 / 149 | 7 | 7 | Oui, après ticks |
| `8675309` | 153 / 112 | 87 / 58 | 153 | 871 | Oui, après ticks |
Chaque groupe réunit deux bassins retenus à des niveaux différents. Le
diagnostic suit leau réelle depuis chaque bouche, horizontalement ou en
descendant, sans mouvement vers le haut, jusquau bassin cible ;
un exutoire terminal sans bassin cible nest pas compté comme cette liaison.
La descente est mesurée sur ce parcours propre à la bouche. Les nouveaux
blocs accessibles en sont les eaux écoulées qui n’étaient ni retenues dans
les bassins ni présentes avant la simulation. Les exutoires terminaux
facultatifs doivent descendre dau moins douze blocs réels.
Les terrasses sont limitées à trois blocs de profondeur et cinq blocs
dincision ; les lacs, à huit blocs de profondeur et douze blocs dincision.
Pour les terrasses, le diagnostic exige aussi quatre blocs dair naturel
au-dessus de chaque colonne excavée, en plus des fonds et supports conservés.
| Graine | Longueur rivière | Déplacement | Niveau Y | Colonnes deau | Colonnes des bassins terminaux | Sources liées |
| --- | --- | --- | --- | --- | --- | --- |
| `0` | 409,8 | 199,7 | 243 | 2 863 | 240 / 241 | 1 |
| `42` | 247,8 | 171,0 | 247 | 1 712 | 254 / 247 | 1 |
| `8675309` | 340,7 | 251,3 | 239 | 2 247 | 265 / 244 | 1 |
La longueur additionne les déplacements du tracé, diagonales comprises. Les
bassins terminaux et larrivée de leur cascade sont contrôlés après les ticks.
| Graine | Colonnes du lac | Blocs deau réels | Profondeur retenue maximale |
| --- | --- | --- | --- |
| `0` | 728 | 3 746 | 8 |
| `42` | 722 | 3 027 | 8 |
| `8675309` | 992 | 4 978 | 8 |
Chaque ligne décrit un site classé `LAKE`, après simulation. Les étangs,
bassins de rivière et terrasses sont consignés dans leurs propres catégories.
| Graine | Eau retenue, geysers inclus | Dont sources de geyser | Eau hors retenues | Eau totale | Lave totale | Sources de lave | Coulées | Accès poche de lave |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 9 845 | 1 | 4 971 | 14 816 | 144 | 20 | 2 | `(96, 100, -45)` |
| `42` | 7 267 | 4 | 903 | 8 170 | 144 | 20 | 2 | `(67, 133, -108)` |
| `8675309` | 10 268 | 5 | 1 230 | 11 498 | 144 | 20 | 2 | `(-93, 156, -21)` |
Ces volumes sont mesurés après 1 800 ticks actifs ; ils ne mesurent pas un
débit et ne signifient pas que les coulées ont atteint leur état final.
Leau retenue correspond au volume attendu après décoration et comprend
les sources propres aux geysers. Les composantes extérieures sont rattachées
aux sources rocheuses ou aux débordements déclarés, avec leurs raccords
réels. La lave des coulées est distinguée de la poche couverte.
| Graine | Sols de mousse | Sols de podzol | Sols de mycélium |
| --- | --- | --- | --- |
| `0` | 13 175 | 10 936 | 5 074 |
| `42` | 4 897 | 3 712 | 2 200 |
| `8675309` | 10 058 | 6 714 | 3 293 |
Les stocks suivants portent sur les seuls chunks inspectés, avant les
ticks de fluides. Les variantes ordinaires et dardoise de chaque minerai
sont additionnées. Ce relevé ne couvre pas toute l’île et ne déclenche
jamais de remplissage pour atteindre un quota.
| Minerai | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Charbon | 7 858 | 6 788 | 4 992 |
| Fer | 3 224 | 2 700 | 2 172 |
| Cuivre | 2 439 | 2 132 | 1 621 |
| Or | 930 | 687 | 507 |
| Redstone | 924 | 631 | 523 |
| Lapis | 278 | 279 | 137 |
| Diamant | 162 | 86 | 88 |
| Émeraude | 64 | 41 | 42 |
| Matière | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Tuf | 657 813 | 491 736 | 239 434 |
| Cobblestone | 285 441 | 172 282 | 162 791 |
| Cobblestone moussue | 593 694 | 420 042 | 229 846 |
| Boue compactée | 168 698 | 167 276 | 158 922 |
| Ardoise des abîmes | 1 443 679 | 901 821 | 858 181 |
| Roche noire | 539 269 | 275 994 | 324 101 |
| Basalte | 605 828 | 390 889 | 307 652 |
| Soufre | 87 811 | 57 838 | 82 209 |
| Cinabre | 11 797 | 13 597 | 20 095 |
La sélection des chunks suit les sites deau et d’écologie ; elle nest pas
un échantillon représentatif de toute l’île. Ces stocks ne démontrent pas
laccessibilité de chaque bloc et ne prouvent pas, seuls, une réduction du
soufre par rapport à une autre version sur un périmètre différent.
Le tableau des minerais extérieurs sélectionne charbon, fer et cuivre ; les
JSON gardent les autres familles effectivement observées avec une face dair.
| Graine | Réentrée source deau | Réentrée source de lave | Réentrée débordement |
| --- | --- | --- | --- |
| `0` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
| `42` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
| `8675309` | 1 vérifiée(s) | 1 vérifiée(s) | 1 vérifiée(s) |
Après consommation des marqueurs, les rappels du post-traitement préservent
les modifications témoins apportées aux bouches ; les blocs sont restaurés
ensuite. Les 176 fichiers historiques suivis hors raccordement
conservent leur SHA-256. Aucun monde personnel nest ouvert par ces tests.
Preuves locales ignorées : `build/alpha9-check-build-final.log` (build et graine 0),
`build/alpha9-seed42-final.log`, `build/alpha9-seed8675309-final.log` et les rapports
`cavern-seed-<graine>-after-1800-ticks.json` et `-postprocessing-reentry.json`
sous `mods/sanctuary/build/run/gameTest/diagnostics/`.
`build/report-alpha9-validation.py` extrait les mesures et vérifie les preuves
cumulées, le gel des sources et les journaux. Le hash final du JAR est lu
dans `build/alpha9-jar-sha256.txt`, sans valeur codée dans lextracteur.
Ces résultats ne garantissent aucune géode, terrasse, rivière, geyser ou
ressource sur chaque graine. Lappréciation visuelle et l’équilibrage en
partie restent ouverts. La publication et les deux synchronisations de
la même instance Prism sont vérifiées séparément lors de la distribution.
## Contrôles et validation alpha.8 — 9 septembre 2026
Les trois essais utilisent des mondes neufs et le preset de production
+19 -5
View File
@@ -61,9 +61,13 @@ petits bassins, sans imposer de courts ruisseaux. Le terrain est traité près d
avec des dépôts épais et des rives variées. De rares sources sortent de niches
rocheuses naturelles et leurs cascades peuvent descendre jusque dans le vide ;
les bassins restent retenus et ne doivent pas provoquer une inondation globale.
Une rivière peut avoir des échappements et des chutes déclarés. Les grands bassins souterrains
remplis d'eau ne sont pas retenus pour cette île. Le terrain doit laisser voir un
dessous sculpté et dégagé, dans un monde de **384 blocs de hauteur**, avec des
Une rivière peut avoir des échappements et des chutes déclarés. Les eaux
intérieures doivent rester localisées et retenues, sans aquifère qui remplit
globalement les cavités. Des bassins naturels étagés, évoquant des rizières,
peuvent se relier par des cascades et offrir des passages verticaux par la nage.
Cette image ne prévoit aucune culture de riz dans le mod Sanctuary.
Le terrain doit laisser voir un dessous sculpté et dégagé, dans un monde de
**384 blocs de hauteur**, avec des
nuages relevés et sans disque noir artificiel apparaissant sous un seuil d'altitude.
Lambiance de départ doit rester douce et tempérée, avec de véritables forêts
@@ -77,8 +81,9 @@ masse artificielle au centre de l’île.
La géologie change en descendant : tuf, cobblestone, pierres moussues et boue
compactée dans les intérieurs humides, puis ardoise des abîmes, roche noire et
basalte. Des poches de soufre doivent être réellement explorables. Quelques
sources profondes peuvent former des coulées de lave ; elles ne doivent pas
basalte. Des poches de soufre doivent être réellement explorables et rester
ponctuelles, avec de vrais geysers actifs lorsque les conditions sy prêtent.
Quelques sources profondes peuvent former des coulées de lave ; elles ne doivent pas
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
minerais à rechercher, avec lor, le diamant, la redstone et le lapis.
@@ -91,6 +96,15 @@ cerisier rose ou une autre de ces essences rares, lorsque le terrain convient.
Les biomes très contrastés de Minecraft 26.3, dont Dappled Forest, restent
réservés aux futurs continents.
Les cavités sous l’île doivent aussi accueillir des ambiances luxuriantes
avec de la végétation lumineuse, des secteurs à stalactites et stalagmites et
des géodes daméthyste. Leur décor doit utiliser les sols et plafonds du relief,
sans reconstruire l’île ni déposer de structures flottantes sans support.
Lalpha.9 introduit ces ambiances et recherche des lacs plus profonds et
volumineux ; les résultats et leurs limites sont documentés dans les contrôles moteur. Ces cavités appartiennent à
lOverworld de Sanctuary et ne remplacent pas le futur projet de dimension
des cavernes.
L’île doit offrir les ressources nécessaires aux premières étapes de Minecraft,
en quantités modestes pour pousser à explorer les expansions lors des grands
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
+110 -7
View File
@@ -5,10 +5,11 @@ entourée de vide sans répétition d'îles à grande distance. C'est une premi
tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le
système d'expansion collective.
Lalpha.8 utilise les paramètres `sanctuary:sanctuary_rift`. Ses contrôles
dans les vrais chunks passent sur les trois graines de référence, avec
1 800 ticks de fluides. Les observations et limites de cette version sont
consignées séparément des générations précédentes dans [Validation](testing.md).
Lalpha.9 utilise les paramètres `sanctuary:sanctuary_cavern`, pour de nouveaux
mondes uniquement. Sa validation moteur passe sur les trois graines de référence. Lalpha.8 conserve
`sanctuary:sanctuary_rift` et ses résultats sur trois graines avec 1 800 ticks
de fluides ; ils ne valident pas la génération v9. Les observations de chaque
version restent séparées dans [Validation](testing.md).
## Créer un monde
@@ -27,7 +28,10 @@ La seed est libre. Modifier `level-type` ne convertit pas un monde existant :
utiliser un nouveau `level-name` pour tester une autre génération. Les fichiers
de test et les sauvegardes personnelles doivent rester distincts.
## Ce qui est généré
## Socle de génération conservé
Les points suivants décrivent le socle livré jusqu’à lalpha.8. Les évolutions
alpha.9 et leur état de validation sont précisés dans la section v9 ci-dessous.
- Le générateur reste celui de Minecraft, `minecraft:noise`.
- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise`
@@ -59,8 +63,10 @@ de test et les sauvegardes personnelles doivent rester distincts.
Dappled Forest est réservé aux futurs continents. Leur végétation et leurs animaux proviennent
de Minecraft 26.3-pre-2. Les nouveaux identifiants ne reprennent pas les tags
de structures vanilla, afin de ne pas placer leurs structures dans le vide.
- Les carvers, géodes, donjons, grands lacs de lave et sources vanilla sont
retirés. Leau de Sanctuary suit son propre plan de surface et de niches
- Jusqu’à lalpha.8, les carvers, géodes, donjons, grands lacs de lave et sources
vanilla sont retirés. Lalpha.9 ajoute une intégration bornée de géodes et
de geysers, sans réactiver lensemble de la décoration souterraine vanilla.
Leau de Sanctuary suit son propre plan de surface et de niches
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
Aucun mod de biomes externe nest nécessaire.
@@ -71,6 +77,103 @@ chaque seed. Laccès à chaque progression vanilla et les ressources renouvel
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
génération, sans corriger une île pour atteindre un quota.
## Cavités et terrasses deau — génération v9
Le nouveau réglage est `sanctuary:sanctuary_cavern`, avec le biome source
`sanctuary:cavern_island` et la règle de matière `sanctuary:cavern_island`.
La densité réutilise `sanctuary:final_density_rift` sans changer cet identifiant
historique. Ses nouvelles données et traitements
doivent rester distincts de `sanctuary:sanctuary_rift` et de toutes les
générations précédentes. Le preset sélectionne la nouvelle génération à la
création ; une mise à jour du JAR ne convertit pas une sauvegarde ni ne
régénère ses chunks. Les cavités restent dans lOverworld sur 384 blocs de
hauteur ; la dimension des cavernes décrite dans la vision nest pas ajoutée.
### Ambiances et ressources
Le ticket conserve les forêts de surface et ajoute des cavités luxuriantes
éclairées par leur végétation, des secteurs à spéléothèmes et des géodes
daméthyste sur des emplacements compatibles. Les sols, plafonds, coquilles
et ouvertures doivent être observés après la décoration réelle : une valeur
de biome ne prouve ni le décor ni son éclairage. Lobjectif est dutiliser
les volumes naturels de l’île, sans remplir ses trous pour accueillir une
structure.
Les dix biomes ont leurs propres identifiants : `cavern_oak_forest`,
`cavern_birch_forest`, `cavern_clearing`, `cavern_dry_woodland`,
`cavern_rocky_heath`, `cavern_dark_grove`, `cavern_bamboo_grove`,
`cavern_sulfur_depths`, `cavern_lush_caves` et `cavern_dripstone_caves`, dans
le namespace `sanctuary`. Les deux nouvelles ambiances luxuriantes et à
spéléothèmes sont limitées à Y<184 par la palette ; leur décoration recherche
des sols entre Y=40 et Y=182, avec trois blocs naturels de support et un
plafond situé de 6 à 48 blocs au-dessus.
Dans les cavités luxuriantes, la mousse remplace le sol compatible et de
petites plantes laccompagnent. Les vraies lianes `cave_vines` et
`cave_vines_plant` pendent sur 3 à 8 blocs ; leurs baies émettent de la lumière
et restent récoltables. Ce décor najoute pas deau. Les spéléothèmes utilisent
les blocs `pointed_dripstone` orientés, sur un appui de `dripstone_block`
remplaçant de la roche existante, avec une longueur de 1 à 4 blocs selon
lespace disponible.
La géode utilise une configuration vanilla compacte avec améthyste,
améthyste bourgeonnante, calcite et basalte lisse. Son enveloppe doit tenir
dans la roche préexistante et un passage de deux blocs de haut la relie à
la cavité, en préservant laméthyste bourgeonnante et les cristaux attachés.
Un chunk sur huit est candidat ; au plus une pose réussie est retenue par
chunk candidat. Cela ne garantit pas de géode sur une graine.
Le soufre devient admissible dans une bande climatique plus étroite sous
Y=176. La réduction de cette sélection peut être testée indépendamment du
terrain ; seule une comparaison des vrais blocs sur un périmètre commun
mesure sa diminution dans l’île. Un geyser éventuel creuse un seul bloc de
sol pour sa source deau, avec `potent_sulfur` dessous et `magma_block` encore
en dessous. Le fond, les quatre parois et la hauteur libre sont contrôlés.
Le cycle du geyser reste celui de Minecraft ; ce décor est distinct des
bassins hydrologiques et son eau doit être comptée séparément.
Leur présence, leur activité, l’éclairage et la cohabitation avec les eaux
et la végétation sont contrôlés dans les mondes de test. Les preuves de ces
éléments sont cumulées sur les trois graines. Un site incompatible peut
être rejeté ; aucun geyser ni stock nest garanti pour chaque graine.
### Lacs et bassins étagés
Le plan hydrologique conserve la recherche de rivière de
lalpha.8 et vise de plus grands lacs de surface. Les cibles actuelles sont
de lordre de **700 à 1 100 colonnes** par grand lac, jusqu’à **7 à 8 blocs**
de profondeur intérieure et une incision limitée à **12 blocs**. Ce sont des
bornes de recherche ; les dimensions réelles sont relevées séparément, sans quota deau à
remplir. Les fonds, les parois et les dépôts exigent du terrain naturel.
Les terrasses sont recherchées dans des cavités entre **Y=56 et Y=208**,
avec deux paliers par groupe et jusqu’à deux groupes facultatifs.
Un palier vise environ **40 à 180 colonnes**, une profondeur de **1 à 3 blocs**
et une incision dau plus **5 blocs**. Le fond doit conserver trois couches
de sédiments et deux couches de support naturel ; le plafond et lespace
disponible sont vérifiés. Les mesures moteur précisent les sites effectivement retenus.
La recherche suit le choix de pente de leau vanilla : parmi les passages
libres, elle privilégie ceux qui rejoignent la chute la plus proche. Un
bassin inférieur doit intercepter ce trajet réel, sans détour creusé pour
le rejoindre. Les sorties entre paliers sont déclarées et les cascades sont formées par les
ticks de fluides Minecraft. Le premier débordement doit atteindre le second
bassin. Celui-ci peut avoir un exutoire terminal qui s’écoule sur les roches
ou vers le vide, sans troisième bassin ajouté. Le générateur ne doit ni peindre une colonne
deau jusquau vide ni colmater un trou pour retenir un bassin. Les volumes
retenus et les liaisons réelles sont vérifiés séparément après décoration
et simulation, y compris aux frontières de chunks. Les cascades peuvent
servir à monter ou descendre selon la nage vanilla ; aucune mécanique
dascenseur supplémentaire nest prévue. « Rizières » décrit seulement
laspect du paysage en terrasses, sans ajouter une culture de riz.
**Validation moteur au 9 septembre 2026 :** les trois graines de référence
passent les contrôles après 1 800 ticks, avec preuves cumulées des terrasses
connectées, geysers actifs, baies lumineuses, spéléothèmes et améthyste
bourgeonnante. Le build complet réussit. Les résultats et leurs limites sont
consignés dans [Validation](testing.md). Aucune présence universelle nest
garantie ; la distribution est vérifiée séparément selon le guide packwiz.
## Failles, géologie et ressources — génération v8
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
+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.8
pack_version=0.1.0-alpha.8
mod_version=0.1.0-alpha.9
pack_version=0.1.0-alpha.9
maven_group=fr.koka.sanctuary
+24
View File
@@ -179,3 +179,27 @@ tasks.register('riftLavaSmoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.RiftLavaSmoke'
}
tasks.named('check') { dependsOn('riftLavaSmoke') }
tasks.register('cavernPaletteSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernPaletteSmoke'
}
tasks.named('check') { dependsOn('cavernPaletteSmoke') }
tasks.register('cavernHydrologySmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernHydrologySmoke'
}
tasks.named('check') { dependsOn('cavernHydrologySmoke') }
tasks.register('cavernLavaSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.CavernLavaSmoke'
}
tasks.named('check') { dependsOn('cavernLavaSmoke') }
@@ -5,6 +5,7 @@ import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import fr.koka.sanctuary.worldgen.IslandShape;
@@ -32,7 +33,8 @@ public final class SanctuaryWorldGameTests {
@GameTest(maxTicks = 3000)
public void generatedSurfaceWaterRemainsContained(GameTestHelper helper) throws java.io.IOException {
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.start(helper);
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.start(helper);
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.start(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.start(helper);
@@ -42,7 +44,8 @@ public final class SanctuaryWorldGameTests {
@GameTest(maxTicks = 200)
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
if (CavernHydrologyRuntime.enabled(generator)) CavernDiagnostics.verifyLegacyIsolation(helper);
else if (RiftHydrologyRuntime.enabled(generator)) RiftDiagnostics.verifyLegacyIsolation(helper);
else if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
else if (LayeredHydrologyRuntime.enabled(generator)) LayeredHydrologyDiagnostics.verifyLegacyIsolation(helper);
else if (NaturalHydrologyRuntime.enabled(generator)) NaturalHydrologyDiagnostics.verifyLegacyIsolation(helper);
@@ -124,13 +127,24 @@ public final class SanctuaryWorldGameTests {
if (chunk.getSection(0).hasOnlyAir()) continue;
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
helper.assertTrue(NaturalHydrologyRuntime.enabled(generator) || LayeredHydrologyRuntime.enabled(generator)
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator),
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.enabled(generator) || CavernHydrologyRuntime.enabled(generator),
"The legacy island must leave its lower margin entirely empty");
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
for (int y = 0; y < 16; y++) {
BlockPos p = new BlockPos(x, y, z);
var state = chunk.getBlockState(p);
if (CavernHydrologyRuntime.enabled(generator)) {
boolean water = state.getFluidState().is(FluidTags.WATER)
&& CavernDiagnostics.isWithinSpringInspectionFootprint(CavernHydrologyRuntime.cavernPlan(helper.getLevel()), x, z);
final int bx = x, bz = z;
boolean lava = state.getFluidState().is(FluidTags.LAVA)
&& CavernHydrologyRuntime.lavaPlan(helper.getLevel()).flowBounds().stream().anyMatch(b ->
bx >= (Math.floorDiv(b.minX(),16) - 1) * 16 && bx <= (Math.floorDiv(b.maxX(),16) + 2) * 16 - 1
&& bz >= (Math.floorDiv(b.minZ(),16) - 1) * 16 && bz <= (Math.floorDiv(b.maxZ(),16) + 2) * 16 - 1);
helper.assertTrue(state.isAir() || water || lava, "Lower margin must remain void except declared fluid falls: " + p);
continue;
}
if (RiftHydrologyRuntime.enabled(generator)) {
boolean water = state.getFluidState().is(FluidTags.WATER)
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
@@ -1,5 +1,9 @@
package fr.koka.sanctuary;
import fr.koka.sanctuary.worldgen.CavernIslandBiomeSource;
import fr.koka.sanctuary.worldgen.CavernGrovesFeature;
import fr.koka.sanctuary.worldgen.CavernDecorationsFeature;
import fr.koka.sanctuary.worldgen.CavernShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.RiftIslandDensity;
import fr.koka.sanctuary.worldgen.RiftIslandBiomeSource;
import fr.koka.sanctuary.worldgen.RiftGrovesFeature;
@@ -45,6 +49,10 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("rift_island"), RiftIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_groves"), RiftGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("rift_shore_sugar_cane"), RiftShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("cavern_island"), CavernIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_groves"), CavernGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_decorations"), CavernDecorationsFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_shore_sugar_cane"), CavernShoreSugarCaneFeature.CODEC);
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.CavernSpringOutlets;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Finish only pending generation markers; completed saved chunks are never repaired on reload. */
@Mixin(LevelChunk.class)
public abstract class CavernSpringOutletMixin {
@Unique
private CavernSpringOutlets.Pending sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureCavernOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingCavernOutlets = CavernSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishCavernOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingCavernOutlets;
sanctuary$pendingCavernOutlets = CavernSpringOutlets.Pending.EMPTY;
CavernSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -5,6 +5,7 @@ import fr.koka.sanctuary.worldgen.NaturalHydrologyRuntime;
import fr.koka.sanctuary.worldgen.LayeredHydrologyRuntime;
import fr.koka.sanctuary.worldgen.WoodlandHydrologyRuntime;
import fr.koka.sanctuary.worldgen.RiftHydrologyRuntime;
import fr.koka.sanctuary.worldgen.CavernHydrologyRuntime;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.Holder;
@@ -29,7 +30,12 @@ public abstract class SurfaceHydrologyMixin {
StructureManager structures, BiomeManager biomeManager, WorldGenRegion region,
Set<Holder<Biome>> biomes, CallbackInfoReturnable<CompletableFuture<ChunkAccess>> callback) {
var generator = (NoiseBasedChunkGenerator) (Object) this;
if (RiftHydrologyRuntime.enabled(generator)) {
if (CavernHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
CavernHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (RiftHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
RiftHydrologyRuntime.apply(generator, randomState, generated);
return generated;
@@ -0,0 +1,34 @@
package fr.koka.sanctuary.worldgen;
/** Alpha.9 keeps the upper woodland climate and specializes sheltered lower layers. */
public final class CavernBiomePalette {
public enum Kind {
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS, LUSH_CAVES, DRIPSTONE_CAVES
}
private CavernBiomePalette() {}
public static Kind select(int blockY, float variation, float moisture) {
double elevation = blockY - Math.clamp(variation, -1.0F, 1.0F) * 16
- Math.clamp(moisture, -1.0F, 1.0F) * 10;
// Small climate pockets replace alpha.8's broad sulfur province. Neither
// mineral pockets nor cave vegetation can take over the upper forest.
if (blockY < 176 && elevation < 168 && variation > 0.22F && variation < 0.42F
&& moisture > -0.42F && moisture < -0.12F) return Kind.SULFUR_DEPTHS;
if (variation > 0.48F) return Kind.ROCKY_HEATH;
if (blockY < 184 && elevation < 180) {
if (moisture > 0.05F && variation > -0.32F) return Kind.LUSH_CAVES;
if (moisture < -0.18F && variation < 0.22F) return Kind.DRIPSTONE_CAVES;
}
if (elevation < 214) {
if (variation < -0.45F && moisture > 0.40F) return Kind.BAMBOO_GROVE;
double shelteredMoisture = moisture + Math.min(0.55, (214 - elevation) / 160);
if (shelteredMoisture > -0.08) return Kind.DARK_GROVE;
}
if (moisture < -0.55F && variation > -0.25F) return Kind.DRY_WOODLAND;
if (Math.abs(variation) < 0.09F && moisture < 0.35F) return Kind.CLEARING;
if (variation < -0.33F) return Kind.BIRCH_FOREST;
return Kind.OAK_FOREST;
}
}
@@ -0,0 +1,296 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.CaveVines;
import net.minecraft.world.level.block.GrowingPlantHeadBlock;
import net.minecraft.world.level.block.PotentSulfurBlock;
import net.minecraft.world.level.block.SpeleothemBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.PotentSulfurState;
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.GeodeFeature;
/** Decorations in existing lower cavities only. All writes belong to the owner's chunk. */
public final class CavernDecorationsFeature implements Feature {
public static final MapCodec<CavernDecorationsFeature> CODEC = MapCodec.unit(CavernDecorationsFeature::new);
private static final ResourceKey<Feature> GEODE = ResourceKey.create(Registries.FEATURE, SanctuaryMod.id("cavern_amethyst_geode"));
private static final Direction[] SIDES = {Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST};
public enum Kind { LUSH, DRIPSTONE, SULFUR, GEODE }
public record Candidate(Kind kind, BlockPos floor, BlockPos ceiling) {
public Candidate { floor = floor.immutable(); ceiling = ceiling.immutable(); }
}
@Override public MapCodec<CavernDecorationsFeature> codec() { return CODEC; }
/** Pure, bounded probes for development diagnostics; no chunk loads or successful-placement promise. */
public static List<Candidate> candidates(NoiseBasedChunkGenerator generator, RandomState state, int chunkX, int chunkZ) {
if (!CavernHydrologyRuntime.enabled(generator) || Math.abs(chunkX) > 17 || Math.abs(chunkZ) > 17) return List.of();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
var biomes = generator.getBiomeSource().createUncachedResolver(state);
List<Candidate> found = new ArrayList<>();
for (int[] offset : new int[][]{{4, 4}, {4, 12}, {12, 4}, {12, 12}, {8, 8}}) {
int x = chunkX * 16 + offset[0], z = chunkZ * 16 + offset[1], floors = 0;
for (int y = 182; y >= 40; y--) {
if (sampler.sample(x, y, z) <= 0 || sampler.sample(x, y + 1, z) > 0
|| sampler.sample(x, y + 2, z) > 0 || sampler.sample(x, y - 1, z) <= 0
|| sampler.sample(x, y - 2, z) <= 0) continue;
int roof = y + 3;
while (roof <= Math.min(230, y + 48) && sampler.sample(x, roof, z) <= 0) roof++;
if (roof > Math.min(230, y + 48) || roof - y < 6
|| sampler.sample(x, roof + 1, z) <= 0) continue;
BlockPos floor = new BlockPos(x, y, z), ceiling = new BlockPos(x, roof, z);
var biome = biomes.getNoiseBiome(x >> 2, (y + 1) >> 2, z >> 2);
if (biome.is(CavernIslandBiomeSource.LUSH_CAVES)) found.add(new Candidate(Kind.LUSH, floor, ceiling));
else if (biome.is(CavernIslandBiomeSource.DRIPSTONE_CAVES)) found.add(new Candidate(Kind.DRIPSTONE, floor, ceiling));
else if (biome.is(CavernIslandBiomeSource.SULFUR_DEPTHS) && y < 176)
found.add(new Candidate(Kind.SULFUR, floor, ceiling));
if (offset[0] == 8 && offset[1] == 8 && Math.floorMod(chunkSeed(state.seed(), chunkX, chunkZ, 0x9501L), 8) == 0)
found.add(new Candidate(Kind.GEODE, floor, ceiling));
if (++floors >= 3) break;
}
}
// Geodes keep their entry free before this feature's moss, grass or pointed
// dripstone can occupy it. Stable sorting preserves every other candidate order.
found.sort(java.util.Comparator.comparingInt(candidate -> candidate.kind() == Kind.GEODE ? 0 : 1));
return List.copyOf(found);
}
@Override public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
var state = level.getLevel().getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
int cx = origin.getX() >> 4, cz = origin.getZ() >> 4;
boolean changed = false, geyser = false, geode = false;
for (Candidate candidate : candidates(noise, state, cx, cz)) {
BlockPos floor = candidate.floor();
if (!stable(level, sampler, floor) || !level.getBlockState(floor.above()).isAir()) continue;
long seed = WoodlandGroveGeometry.seed(state.seed(), floor.getX(), floor.getY(), floor.getZ(), 0x9502L);
switch (candidate.kind()) {
case LUSH -> changed |= lush(level, noise, state, sampler, candidate, seed);
case DRIPSTONE -> changed |= dripstone(level, noise, state, sampler, candidate, seed);
case SULFUR -> {
if (!geyser && Math.floorMod(chunkSeed(state.seed(), cx, cz, 0x9503L), 2) == 0)
changed |= geyser = geyser(level, noise, state, sampler, candidate);
}
case GEODE -> { if (!geode) changed |= geode = geode(level, noise, state, sampler, candidate, seed); }
}
}
return changed;
}
private boolean lush(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos floor = site.floor(), roof = site.ceiling();
int gap = roof.getY() - floor.getY();
if (CavernHydrologyRuntime.protects(noise, state, floor, 3, gap + 2, 3)) return false;
boolean changed = false;
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) {
if (dx * dx + dz * dz > 5) continue;
BlockPos p = floor.offset(dx, 0, dz);
if (!stable(level, sampler, p) || !stable(level, sampler, p.below()) || !stable(level, sampler, p.below(2))
|| !level.getBlockState(p.above()).isAir()) continue;
setBlock(level, p, Blocks.MOSS_BLOCK.defaultBlockState());
long local = WoodlandGroveGeometry.seed(seed, p.getX(), p.getY(), p.getZ(), 0x9504L);
BlockState plant = (Math.floorMod(local, 5) == 0 ? Blocks.AZALEA : Math.floorMod(local, 3) == 0
? Blocks.MOSS_CARPET : Blocks.SHORT_GRASS).defaultBlockState();
if (plant.canSurvive(level, p.above())) setBlock(level, p.above(), plant);
changed = true;
}
// Real cave-vine heads and bodies remain harvestable and luminous via BERRIES.
int length = Math.min(gap - 3, 3 + Math.floorMod(seed, 6));
if (stable(level, sampler, roof) && stable(level, sampler, roof.above())) {
boolean free = true;
for (int d = 1; d <= length; d++) free &= level.getBlockState(roof.below(d)).isAir();
if (free) for (int d = 1; d <= length; d++) {
BlockPos p = roof.below(d);
BlockState vine = (d == length ? Blocks.CAVE_VINES : Blocks.CAVE_VINES_PLANT).defaultBlockState()
.setValue(CaveVines.BERRIES, d == length || Math.floorMod(seed + d, 3) == 0);
if (d == length) vine = vine.setValue(GrowingPlantHeadBlock.AGE, 25);
if (!vine.canSurvive(level, p)) break;
setBlock(level, p, vine); changed = true;
}
}
return changed;
}
private boolean dripstone(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos floor = site.floor(), roof = site.ceiling();
int gap = roof.getY() - floor.getY();
if (CavernHydrologyRuntime.protects(noise, state, floor, 1, gap + 2, 3)) return false;
int length = Math.min((gap - 3) / 2, 1 + Math.floorMod(seed, 4));
boolean changed = spike(level, sampler, floor, Direction.UP, length);
changed |= spike(level, sampler, roof, Direction.DOWN, length);
return changed;
}
private boolean spike(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos support, Direction direction, int length) {
if (length < 1 || !stable(level, sampler, support) || !stable(level, sampler, support.relative(direction.getOpposite()))) return false;
for (int d = 1; d <= length; d++) {
BlockPos p = support.relative(direction, d);
if (!level.getBlockState(p).isAir() || sampler.sample(p.getX(), p.getY(), p.getZ()) > 0) return false;
}
setBlock(level, support, Blocks.DRIPSTONE_BLOCK.defaultBlockState());
for (int d = 1; d <= length; d++) {
SpeleothemThickness thickness = d == length ? SpeleothemThickness.TIP
: d == length - 1 ? SpeleothemThickness.FRUSTUM : d == 1 ? SpeleothemThickness.BASE : SpeleothemThickness.MIDDLE;
BlockState pointed = Blocks.POINTED_DRIPSTONE.defaultBlockState().setValue(SpeleothemBlock.TIP_DIRECTION, direction)
.setValue(SpeleothemBlock.THICKNESS, thickness);
setBlock(level, support.relative(direction, d), pointed);
}
return true;
}
private boolean geyser(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site) {
BlockPos water = site.floor(), potent = water.below(), heat = water.below(2);
if (site.ceiling().getY() - water.getY() < 8
|| CavernHydrologyRuntime.protects(noise, state, water, 2, 8, 5)) return false;
for (int d = 0; d <= 4; d++) if (!stable(level, sampler, water.below(d))) return false;
for (Direction side : SIDES) for (int d = 0; d <= 2; d++)
if (!stable(level, sampler, water.below(d).relative(side))) return false;
for (int d = 1; d <= 7; d++) if (!level.getBlockState(water.above(d)).isAir()) return false;
// One source, four original rock walls, original deep support. Vanilla's
// periodic geyser ticker requires magma below potent sulfur and source water above.
setBlock(level, heat, Blocks.MAGMA_BLOCK.defaultBlockState());
setBlock(level, water, Blocks.WATER.defaultBlockState());
setBlock(level, potent, Blocks.POTENT_SULFUR.defaultBlockState().setValue(PotentSulfurBlock.STATE, PotentSulfurState.DORMANT));
level.getChunk(water).markPosForPostProcessing(water);
level.getChunk(potent).markPosForPostProcessing(potent);
return true;
}
private boolean geode(WorldGenLevel level, NoiseBasedChunkGenerator noise, RandomState state,
WoodlandGroveGeometry.Sampler sampler, Candidate site, long seed) {
BlockPos origin = site.floor().offset(-1, -9, -1);
// Include the vanilla write range, its crystal neighbours and the entrance.
// This asymmetric envelope is centred explicitly so its positive edge is protected too.
if (origin.getY() - 6 < 24 || !level.getBlockState(site.floor().above(2)).isAir()
|| CavernHydrologyRuntime.protects(noise, state, origin.offset(1, -6, 1), 7, 17, 0)) return false;
Map<BlockPos, BlockState> before = new LinkedHashMap<>();
for (int dx = -6; dx <= 8; dx++) for (int dz = -6; dz <= 8; dz++) for (int dy = -6; dy <= 11; dy++) {
BlockPos p = origin.offset(dx, dy, dz);
BlockState original = level.getBlockState(p);
// No block-entity data or fluid scheduler state needs to be restored.
if (original.hasBlockEntity() || !original.getFluidState().isEmpty()) return false;
before.put(p, original);
}
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(GEODE).value();
if (!(feature instanceof GeodeFeature geode) || geode.minGenOffset() != -5 || geode.maxGenOffset() != 7) return false;
boolean accepted = false;
try {
if (!geode.place(level, noise, RandomSource.create(seed), origin) || !naturalGeodeChanges(level, sampler, before)) return false;
var entrance = geodeEntrance(level, sampler, site.floor().above(), origin, before);
if (entrance.isEmpty()) return false;
for (BlockPos p : entrance) if (!level.getBlockState(p).isAir()) setBlock(level, p, Blocks.AIR.defaultBlockState());
if (!naturalGeodeChanges(level, sampler, before)) return false;
accepted = true;
return true;
} finally {
if (!accepted) before.forEach((p, original) -> {
if (!level.getBlockState(p).equals(original)) setBlock(level, p, original);
});
}
}
/** Test the actual vanilla mask, not unused bounding-box corners. A geode may
* excavate original rock but cannot fill original air or remove earlier decoration. */
private static boolean naturalGeodeChanges(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
Map<BlockPos, BlockState> before) {
for (var entry : before.entrySet()) {
BlockPos p = entry.getKey();
BlockState original = entry.getValue(), current = level.getBlockState(p);
if (original.equals(current)) continue;
if (!carvableOriginal(level, sampler, p, original)) return false;
}
return true;
}
private static boolean carvableOriginal(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos p, BlockState original) {
return original != null && original.isCollisionShapeFullBlock(level, p)
&& !original.hasBlockEntity() && original.getFluidState().isEmpty()
&& !original.is(BlockTags.LOGS) && !original.is(BlockTags.LEAVES)
&& !original.is(Blocks.BUDDING_AMETHYST) && !original.is(Blocks.MAGMA_BLOCK)
&& sampler.sample(p.getX(), p.getY(), p.getZ()) > 0;
}
private static List<BlockPos> geodeEntrance(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos start, BlockPos origin, Map<BlockPos, BlockState> before) {
var previous = new java.util.HashMap<BlockPos, BlockPos>();
var distances = new java.util.HashMap<BlockPos, Integer>();
var queue = new java.util.ArrayDeque<BlockPos>();
queue.add(start); distances.put(start, 0);
BlockPos destination = null;
while (!queue.isEmpty()) {
BlockPos p = queue.removeFirst();
int distance = distances.get(p);
boolean adjacentBudding = false;
for (Direction direction : Direction.values())
adjacentBudding |= level.getBlockState(p.relative(direction)).is(Blocks.BUDDING_AMETHYST)
|| level.getBlockState(p.above().relative(direction)).is(Blocks.BUDDING_AMETHYST);
if (p.getY() <= start.getY() - 3 && level.getBlockState(p).isAir()
&& level.getBlockState(p.above()).isAir() && adjacentBudding) { destination = p; break; }
if (distance >= 16) continue;
for (Direction direction : Direction.values()) {
BlockPos next = p.relative(direction);
if (Math.abs(next.getX() - start.getX()) > 6 || Math.abs(next.getZ() - start.getZ()) > 6
|| next.getY() < origin.getY() - 5 || next.getY() > start.getY()
|| distances.containsKey(next) || !before.containsKey(next) || !before.containsKey(next.above())) continue;
if (!entranceBlock(level, sampler, next, before) || !entranceBlock(level, sampler, next.above(), before)) continue;
distances.put(next, distance + 1); previous.put(next, p); queue.addLast(next);
}
}
if (destination == null) return List.of();
var passage = new java.util.LinkedHashSet<BlockPos>();
for (BlockPos p = destination; p != null; p = previous.get(p)) { passage.add(p); passage.add(p.above()); }
return List.copyOf(passage);
}
private static boolean entranceBlock(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler,
BlockPos p, Map<BlockPos, BlockState> before) {
BlockState current = level.getBlockState(p);
if (current.isAir()) return true;
// Keep every budding block and attached crystal. Search around them instead.
if (current.is(Blocks.BUDDING_AMETHYST) || current.is(Blocks.AMETHYST_CLUSTER)
|| current.is(Blocks.SMALL_AMETHYST_BUD) || current.is(Blocks.MEDIUM_AMETHYST_BUD)
|| current.is(Blocks.LARGE_AMETHYST_BUD)) return false;
for (Direction direction : Direction.values()) {
BlockState neighbour = level.getBlockState(p.relative(direction));
if (neighbour.getBlock() instanceof AmethystClusterBlock
&& neighbour.getValue(AmethystClusterBlock.FACING) == direction) return false;
}
return carvableOriginal(level, sampler, p, before.get(p));
}
private static boolean stable(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, BlockPos p) {
BlockState state = level.getBlockState(p);
return sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 && state.isCollisionShapeFullBlock(level, p)
&& state.getFluidState().isEmpty() && !state.hasBlockEntity() && !state.is(BlockTags.LOGS)
&& !state.is(BlockTags.LEAVES) && !state.is(Blocks.BUDDING_AMETHYST) && !state.is(Blocks.MAGMA_BLOCK);
}
private static long chunkSeed(long seed, int x, int z, long salt) {
return WoodlandGroveGeometry.seed(seed, x * 16, 0, z * 16, salt);
}
}
@@ -0,0 +1,296 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.data.worldgen.features.VegetationFeatures;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
/** Alpha.9: sheltered vanilla mushroom trees and dark oak on natural lower floors,
* plus one globally selected remarkable tree.
* Placement runs only in new generation. No world journal or mutable placement counter is retained. */
public final class CavernGrovesFeature implements Feature {
public static final MapCodec<CavernGrovesFeature> CODEC = MapCodec.unit(CavernGrovesFeature::new);
private static final Map<RandomState, Optional<RemarkableTree>> REMARKABLE =
Collections.synchronizedMap(new WeakHashMap<>());
public record RemarkableTree(BlockPos base, String featureId, int radius, int height) {
public RemarkableTree { base = base.immutable(); }
}
private record Species(ResourceKey<Feature> key, int width, int radius, int height) {}
private record Candidate(int x, int z, long rank) {}
@FunctionalInterface
private interface Protection {
boolean intersects(BlockPos base, int radius, int height, int rootDepth);
}
private static final Species CHERRY = new Species(TreeFeatures.CHERRY, 1, 9, 15);
private static final Species[] SPECIES = {CHERRY, CHERRY, CHERRY, CHERRY,
new Species(TreeFeatures.SPRUCE, 1, 5, 15), new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14),
new Species(TreeFeatures.ACACIA, 1, 7, 14), new Species(TreeFeatures.MANGROVE, 1, 10, 20),
new Species(TreeFeatures.PALE_OAK_BONEMEAL, 2, 6, 12)};
@Override public MapCodec<CavernGrovesFeature> codec() { return CODEC; }
public static Optional<RemarkableTree> remarkablePlan(ServerLevel level) {
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)) return Optional.empty();
return remarkablePlan(noise, level.getChunkSource().randomState());
}
public static Optional<RemarkableTree> remarkablePlan(NoiseBasedChunkGenerator noise, RandomState state) {
if (!CavernHydrologyRuntime.enabled(noise)) return Optional.empty();
return REMARKABLE.computeIfAbsent(state, ignored -> {
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
Species species = SPECIES[Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), 0, 0, 0, 0x70EEL), SPECIES.length)];
var candidates = new ArrayList<Candidate>();
for (int x = -152; x <= 152; x += 16) for (int z = -152; z <= 152; z += 16) {
if (Math.hypot(x, z) <= 168) candidates.add(new Candidate(x, z,
WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x71AFL)));
}
candidates.sort(Comparator.comparingLong(Candidate::rank).thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
for (Candidate candidate : candidates) {
int bed = WoodlandGroveGeometry.highestSurface(sampler, candidate.x(), candidate.z());
if (!WoodlandGroveGeometry.naturalFootprint(sampler, candidate.x(), bed, candidate.z(), species.width())) continue;
BlockPos base = new BlockPos(candidate.x(), bed + 1, candidate.z());
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))) continue;
if (!WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(),
species.width(), species.radius(), species.height())) continue;
return Optional.of(new RemarkableTree(base, species.key().identifier().toString(), species.radius(), species.height()));
}
return Optional.empty();
});
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource ignored, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
int minX = (origin.getX() >> 4) * 16, minZ = (origin.getZ() >> 4) * 16;
if (Math.abs((long) minX) > 272 || Math.abs((long) minZ) > 272) return false;
var state = level.getLevel().getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
WoodlandGroveGeometry.Sampler sampler = (x, y, z) -> state.sampleBlockValueUncached(density, x, y, z);
Protection protection = (base, radius, height, rootDepth) ->
CavernHydrologyRuntime.protects(noise, state, base, radius, height, rootDepth);
boolean changed = false;
int[] heights = new int[256];
Arrays.fill(heights, Integer.MIN_VALUE);
var remarkable = remarkablePlan(noise, state);
if (remarkable.isPresent()) {
var tree = remarkable.get();
BlockPos base = tree.base();
if ((base.getX() >> 4) == (minX >> 4) && (base.getZ() >> 4) == (minZ >> 4)) {
for (Species species : SPECIES) if (species.key().identifier().toString().equals(tree.featureId())) {
changed |= tree(level, generator, sampler, state.seed(), base, species, protection, 0x723BL);
break;
}
}
}
// The scarce tall rooms usually go to dark oak first; one quarter of chunks
// give mushroom trees first choice. This changes priority, never attempt budgets
// or natural fit requirements, and uses a separate deterministic chunk seed.
boolean mushroomsFirst = Math.floorMod(WoodlandGroveGeometry.seed(state.seed(), minX, 0, minZ, 0x8503L), 4) == 0;
if (mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Four spaced origins per chunk; the 2x2 trunk and full canopy still have to fit the cave.
for (int x = minX + 4; x < minX + 16; x += 8) for (int z = minZ + 4; z < minZ + 16; z += 8) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), placed = 0;
for (int bed = highest - 8; bed >= 40; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()) continue;
var biome = level.getBiome(base);
boolean dark = biome.is(CavernIslandBiomeSource.DARK_GROVE);
boolean bamboo = biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
if (!dark && !bamboo) continue;
long placementSeed = WoodlandGroveGeometry.seed(state.seed(), x, bed, z, 0x730DL);
if (dark) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.DARK_OAK, 2, 6, 12), protection, 0x730DL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 8) == 0) {
if (tree(level, generator, sampler, state.seed(), base,
new Species(TreeFeatures.JUNGLE_TREE_NO_VINE, 1, 5, 14), protection, 0x735BL)) { changed = true; placed++; }
} else if (Math.floorMod(placementSeed, 3) == 0
&& WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1)
&& WoodlandGroveGeometry.treeRoom(sampler, x, bed + 1, z, 1, 3, 16)
&& !blocked(protection, base, 4, 17) && actualRoom(level, base, 1, 3, 16)) {
setBlock(level, base.below(), Blocks.PODZOL.defaultBlockState());
changed |= level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(VegetationFeatures.BAMBOO_NO_PODZOL)
.value().place(level, generator, RandomSource.create(placementSeed), base);
placed++;
}
if (placed >= 2) break;
}
}
if (!mushroomsFirst) changed |= mushroomTrees(level, generator, sampler, state.seed(), protection, minX, minZ, heights);
// Coherent ground patches: about 40% moss, 40% podzol, 20% mycelium.
// Small mushrooms are accents (one opportunity per hundred columns), not the canopy.
for (int x = minX; x < minX + 16; x++) for (int z = minZ; z < minZ + 16; z++) {
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights), floors = 0;
long detail = WoodlandGroveGeometry.seed(state.seed(), x, 0, z, 0x843EL);
boolean smallMushroomPlaced = false;
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos floor = new BlockPos(x, bed, z), plant = floor.above();
if (!ground(level, floor) || !level.getBlockState(plant).isAir()) continue;
var biome = level.getBiome(plant);
if (!biome.is(CavernIslandBiomeSource.DARK_GROVE) && !biome.is(CavernIslandBiomeSource.BAMBOO_GROVE)) continue;
if (!WoodlandGroveGeometry.naturalFootprint(sampler, x, bed, z, 1) || blocked(protection, plant, 1, 2)) continue;
long patch = WoodlandGroveGeometry.seed(state.seed(), Math.floorDiv(x, 7), 0, Math.floorDiv(z, 7), 0x742DL);
int soilKind = Math.floorMod(patch, 5);
BlockState soil = (soilKind < 2 ? Blocks.MOSS_BLOCK : soilKind < 4 ? Blocks.PODZOL : Blocks.MYCELIUM).defaultBlockState();
setBlock(level, floor, soil);
if (!smallMushroomPlaced && Math.floorMod(detail, 100) == 0) {
BlockState mushroom = (Math.floorMod(detail >>> 8, 2) == 0 ? Blocks.BROWN_MUSHROOM : Blocks.RED_MUSHROOM).defaultBlockState();
if (mushroom.canSurvive(level, plant)) {
setBlock(level, plant, mushroom);
smallMushroomPlaced = true;
}
}
changed = true;
if (++floors >= 3) break;
}
}
return changed;
}
private boolean mushroomTrees(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler,
long seed, Protection protection, int minX, int minZ, int[] heights) {
boolean changed = false;
RandomSource mushroomSites = RandomSource.create(WoodlandGroveGeometry.seed(seed, minX, 0, minZ, 0x8501L));
int mushroomAttempts = 1 + mushroomSites.nextInt(3);
for (int attempt = 0; attempt < mushroomAttempts; attempt++) {
int x = minX + 3 + mushroomSites.nextInt(10), z = minZ + 3 + mushroomSites.nextInt(10);
int highest = localTop(level, sampler, protection, minX, minZ, x, z, heights);
for (int bed = highest - 8; bed >= 32; bed--) {
BlockPos base = new BlockPos(x, bed + 1, z);
if (!ground(level, base.below()) || !level.getBlockState(base).isAir()
|| !level.getBiome(base).is(CavernIslandBiomeSource.DARK_GROVE)) continue;
long mushroomSeed = WoodlandGroveGeometry.seed(seed, x, bed, z, 0x8502L + attempt);
var key = Math.floorMod(mushroomSeed, 2) == 0 ? TreeFeatures.HUGE_BROWN_MUSHROOM : TreeFeatures.HUGE_RED_MUSHROOM;
Feature feature = level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(key).value();
if (!(feature instanceof AbstractHugeMushroomFeature mushroom)) break;
int height = mushroom.getTreeHeight(RandomSource.create(mushroomSeed));
int radius = mushroom.foliageRadius();
if (!mushroomRoom(level, sampler, protection, base, radius, height)) continue;
BlockState originalFloor = level.getBlockState(base.below());
setBlock(level, base.below(), (Math.floorMod(mushroomSeed, 5) == 0 ? Blocks.MYCELIUM : Blocks.PODZOL).defaultBlockState());
// Vanilla consumes its height draw first. Replaying the same random seed
// gives precisely the height whose natural envelope was checked above.
boolean placed = mushroom.place(level, generator, RandomSource.create(mushroomSeed), base);
if (!placed) setBlock(level, base.below(), originalFloor);
changed |= placed;
break;
}
}
return changed;
}
private boolean tree(WorldGenLevel level, ChunkGenerator generator, WoodlandGroveGeometry.Sampler sampler, long seed,
BlockPos base, Species species, Protection protection, long salt) {
if (blocked(protection, base, species.radius() + 1, species.height(), rootDepth(species))
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), species.width())
|| !actualRoom(level, base, species.width(), species.radius(), species.height())
|| !WoodlandGroveGeometry.treeRoom(sampler, base.getX(), base.getY(), base.getZ(), species.width(), species.radius(), species.height())) return false;
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
BlockPos floor = base.offset(dx, -1, dz);
if (!ground(level, floor)) return false;
}
for (int dx = 0; dx < species.width(); dx++) for (int dz = 0; dz < species.width(); dz++) {
setBlock(level, base.offset(dx, -1, dz), Blocks.PODZOL.defaultBlockState());
}
return level.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(species.key()).value()
.place(level, generator, RandomSource.create(WoodlandGroveGeometry.seed(seed, base.getX(), base.getY(), base.getZ(), salt)), base);
}
private static boolean actualRoom(WorldGenLevel level, BlockPos base, int width, int radius, int height) {
for (int y = 0; y <= height; y++) {
int from = y < 3 ? 0 : -radius, to = y < 3 ? width - 1 : radius;
for (int dx = from; dx <= to; dx++) for (int dz = from; dz <= to; dz++) {
BlockState current = level.getBlockState(base.offset(dx, y, dz));
if (!current.getFluidState().isEmpty() || !(current.isAir() || current.is(BlockTags.LEAVES)
|| current.getBlock() instanceof VegetationBlock)) return false;
}
}
return true;
}
private static boolean ground(WorldGenLevel level, BlockPos floor) {
BlockState state = level.getBlockState(floor);
if (state.is(Blocks.DEEPSLATE)) {
// Deep humid ledges can support organic soil. Check every tree foot rather
// than admitting this rock globally, including for the remarkable tree.
var biome = level.getBiome(floor.above());
return biome.is(CavernIslandBiomeSource.DARK_GROVE) || biome.is(CavernIslandBiomeSource.BAMBOO_GROVE);
}
return state.is(Blocks.STONE) || state.is(Blocks.DIRT) || state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.PODZOL)
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.COARSE_DIRT)
|| state.is(Blocks.GRANITE) || state.is(Blocks.DIORITE) || state.is(Blocks.ANDESITE) || state.is(Blocks.TUFF)
|| state.is(Blocks.COBBLESTONE) || state.is(Blocks.MOSSY_COBBLESTONE) || state.is(Blocks.PACKED_MUD);
}
private static int localTop(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
int minX, int minZ, int x, int z, int[] heights) {
int index = (x - minX) * 16 + z - minZ;
if (heights[index] != Integer.MIN_VALUE) return heights[index];
// In an untouched column the heightmap bounds the original surface cheaply.
// A protected water/lava column may have been carved: recover its raw top
// without depending on the current hydrology plan's representation.
int ceiling = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - 1;
if (protection.intersects(new BlockPos(x, level.getMinY(), z), 0, level.getHeight() - 1, 0))
ceiling = level.getMaxY();
return heights[index] = WoodlandGroveGeometry.highestSurfaceBelow(sampler, x, z, ceiling);
}
private static boolean mushroomRoom(WorldGenLevel level, WoodlandGroveGeometry.Sampler sampler, Protection protection,
BlockPos base, int radius, int height) {
if (blocked(protection, base, radius + 1, height + 1, 4)
|| !WoodlandGroveGeometry.naturalFootprint(sampler, base.getX(), base.getY() - 1, base.getZ(), 1)) return false;
for (int depth = 1; depth <= 3; depth++) {
BlockPos floor = base.below(depth);
if (!level.getBlockState(floor).isCollisionShapeFullBlock(level, floor)) return false;
}
// A complete conservative column around the cap handles both brown umbrellas
// and the lower rounded red cap. Neither natural rock nor existing logs move.
for (int y = 0; y <= height + 1; y++) for (int dx = -radius; dx <= radius; dx++) for (int dz = -radius; dz <= radius; dz++) {
BlockPos p = base.offset(dx, y, dz);
BlockState current = level.getBlockState(p);
if (sampler.sample(p.getX(), p.getY(), p.getZ()) > 0 || !current.getFluidState().isEmpty()
|| !(current.isAir() || current.is(BlockTags.LEAVES))) return false;
}
// Being below another surface is not enough: the same column needs an actual
// original rock ceiling above the whole mushroom, not an open hillside.
for (int y = base.getY() + height + 2; y <= level.getMaxY(); y++)
if (sampler.sample(base.getX(), y, base.getZ()) > 0) return true;
return false;
}
private static int rootDepth(Species species) { return species.key().equals(TreeFeatures.MANGROVE) ? 16 : 4; }
private static boolean blocked(Protection protection, BlockPos base, int radius, int height) {
return blocked(protection, base, radius, height, 4);
}
private static boolean blocked(Protection protection, BlockPos base, int radius, int height, int rootDepth) {
return protection.intersects(base, radius, height, rootDepth);
}
}
@@ -0,0 +1,966 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
/** Alpha.9 river catchments, wider basins and rocky wall springs.
* This version is separate from NaturalHydrology so previously created worlds keep their original terrain.
* Results retain no chunks, sampler, random state or Minecraft objects. */
public final class CavernHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 16;
public static final int MAX_DEPTH = 8;
public static final int MAX_LAKE_CARVE = 12;
public static final int MAX_RIVER_BANK_CARVE = 12;
private static final int GRID = 8;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private CavernHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER, TERRACE }
public enum ShoreMaterial { SAND, GRAVEL, CLAY, STONE, GRASS }
public record Point(int x, int z) {}
public record Bounds(int minX, int minZ, int maxX, int maxZ) {}
public record Position(int x, int y, int z) {}
public record FlowBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {}
public record Spring(long id, Position source, Position outlet, List<Position> flowPath, FlowBounds flowBounds) {
public Spring { flowPath = List.copyOf(flowPath); }
}
public record Terrace(long id, List<Long> basinIds) {
public Terrace { basinIds = List.copyOf(basinIds); }
}
/** A retained upper basin supplies this outlet. toFeatureId=-1 denotes an open terminal fall. */
public record Spill(long id, long fromFeatureId, long toFeatureId, Position source, Position outlet,
List<Position> flowPath, FlowBounds flowBounds) {
public Spill { flowPath = List.copyOf(flowPath); }
}
/** Replaces existing solids bedY-sedimentDepth+1..bedY, with two intact natural supports below.
* Water occupies bedY+1..waterY; everything above it through carveTop is removed. For a dry
* terrace waterY=-1, and bedY+1..carveTop is removed (at most two original blocks). */
public record Cell(int x, int z, int waterY, int bedY, int carveTop,
ShoreMaterial material, long featureId, int sedimentDepth) {
public boolean hasWater() { return waterY >= 0; }
}
public record Feature(long id, Kind kind, int waterY, List<Point> path, int waterCells, Bounds bounds) {
public Feature { path = List.copyOf(path); }
}
public static final class Plan {
private final Map<Long, Cell> cells;
private final List<Cell> allCells;
private final Map<Long, List<Cell>> layers;
private final Map<Long, List<Cell>> chunks;
private final List<Feature> features;
private final List<Spring> springs;
private final Map<Long, List<Spring>> springChunks;
private final Set<Position> springFlow;
private final int sampledColumns;
private final long densitySamples;
private final List<Terrace> terraces;
private final List<Spill> spills;
private final Set<Position> spillOpenings;
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
this(source, features, springs, springFlow, columns, samples, CavernTerraces.Result.EMPTY);
}
private Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow,
int columns, long samples, CavernTerraces.Result lower) {
List<Cell> combined = new ArrayList<>(source.values());
combined.addAll(lower.cells());
List<Cell> sorted = combined.stream()
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z).thenComparingInt(Cell::bedY)).toList();
allCells = List.copyOf(sorted);
Map<Long, Cell> ordered = new LinkedHashMap<>();
Map<Long, List<Cell>> stacked = new HashMap<>();
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : sorted) {
ordered.put(key(cell.x(), cell.z()), cell);
stacked.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4), ignored -> new ArrayList<>()).add(cell);
}
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
cells = Collections.unmodifiableMap(ordered);
stacked.replaceAll((ignored, values) -> List.copyOf(values));
layers = Map.copyOf(stacked);
chunks = Map.copyOf(byChunk);
List<Feature> allFeatures = new ArrayList<>(features);
allFeatures.addAll(lower.features());
this.features = List.copyOf(allFeatures);
this.springs = List.copyOf(springs);
Set<Position> allFlow = new HashSet<>(springFlow);
allFlow.addAll(lower.predictedFlow());
this.springFlow = Set.copyOf(allFlow);
terraces = List.copyOf(lower.terraces());
spills = List.copyOf(lower.spills());
spillOpenings = spills.stream().map(Spill::outlet).collect(java.util.stream.Collectors.toUnmodifiableSet());
Map<Long, List<Spring>> groupedSprings = new HashMap<>();
for (Spring spring : springs) groupedSprings.computeIfAbsent(
key(spring.source().x() >> 4, spring.source().z() >> 4), ignored -> new ArrayList<>()).add(spring);
groupedSprings.replaceAll((ignored, values) -> List.copyOf(values));
springChunks = Map.copyOf(groupedSprings);
sampledColumns = columns;
densitySamples = samples;
}
public Cell cellAt(int x, int z) { return cells.get(key(x, z)); }
public List<Cell> cellsAt(int x, int z) { return layers.getOrDefault(key(x, z), List.of()); }
/** Return the modified vertical interval, including its two unchanged support layers. */
public Cell cellAt(int x, int y, int z) {
for (Cell cell : cellsAt(x, z)) if (y >= cell.bedY() - cell.sedimentDepth() - 1
&& y <= Math.max(cell.waterY(), cell.carveTop())) return cell;
return null;
}
public Collection<Cell> cells() { return allCells; }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Feature> features() { return features; }
public boolean isRiverCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.RIVER && f.id() == cell.featureId()); }
public boolean isTerraceCell(Cell cell) { return features.stream().anyMatch(f -> f.kind() == Kind.TERRACE && f.id() == cell.featureId()); }
public List<Terrace> terraces() { return terraces; }
public List<Spill> spills() { return spills; }
public boolean isSpillOpening(int x, int y, int z) { return spillOpenings.contains(new Position(x, y, z)); }
public List<Spring> springs() { return springs; }
public List<Spring> springsInChunk(int x, int z) { return springChunks.getOrDefault(key(x, z), List.of()); }
/** Prediction in undecorated density, not a restriction on Minecraft fluid simulation. */
public boolean allowsSpringFlow(int x, int y, int z) { return springFlow.contains(new Position(x, y, z)); }
public int sampledColumns() { return sampledColumns; }
public long densitySamples() { return densitySamples; }
}
public static Plan create(long seed, Sampler sampler) {
return new Planner(seed, sampler).build();
}
private record Column(int top, int solidBottom) {}
private record Candidate(int x, int z, double score) {}
private record Node(int x, int z, double cost) {}
private static final Comparator<Node> NODE_ORDER = Comparator.comparingDouble(Node::cost)
.thenComparingInt(Node::x).thenComparingInt(Node::z);
private static final class Planner {
private final long seed;
private final Sampler sampler;
private final Map<Long, Column> columns = new HashMap<>();
private final Map<Long, Integer> coarse = new HashMap<>();
private final Map<Long, Cell> cells = new HashMap<>();
private final List<Feature> features = new ArrayList<>();
private final List<Spring> springs = new ArrayList<>();
private final Set<Position> springFlow = new HashSet<>();
private long samples;
Planner(long seed, Sampler sampler) { this.seed = seed; this.sampler = sampler; }
Plan build() {
List<Candidate> candidates = candidates();
addRiver();
int attempts = 0;
for (Candidate candidate : candidates) {
if (features.size() >= 4 || attempts++ >= 96) break;
Point center = new Point(candidate.x(), candidate.z());
if (nearWater(center.x(), center.z(), 38)) continue;
Column column = column(center.x(), center.z());
if (column.top() < 64 || column.top() - column.solidBottom() < 7) continue;
Kind kind = features.size() < 2 ? Kind.LAKE : Kind.POND;
int target = kind == Kind.LAKE ? 700 + (int) (random(seed, center.x(), center.z()) * 400)
: 100 + (int) (random(seed + 1, center.x(), center.z()) * 140);
// Prefer an existing low spot; a shallow cut is the fallback on level terrain.
for (int level : new int[]{column.top() + 1, column.top(), column.top() - 1, column.top() - 2}) {
Set<Long> footprint = growBasin(center, level, target);
if (footprint == null) continue;
long id = featureId(features.size());
Map<Long, Cell> wet = kind == Kind.LAKE ? lakeCells(footprint, level, id) : waterCells(footprint, level, id);
if (wet == null || !contained(wet)) continue;
addFeature(id, kind, level, List.of(center), wet);
break;
}
}
addRiverSpring();
addSedimentSheets();
addLowerWallSprings();
addSprings();
Plan surface = new Plan(cells, features, springs, springFlow, columns.size(), samples);
var lower = CavernTerraces.create(seed, sampler, surface);
return new Plan(cells, features, springs, springFlow, columns.size(), samples, lower);
}
/** A connected catchment is excavated only in a thick natural plateau. The route is
* selected on the actual fissured density, not painted across missing rock. Its two
* terminal pools share the same retained waterline; a separate rock spring feeds it. */
private void addRiver() {
List<Integer> heights = coarse.values().stream().filter(y -> y >= 160).sorted().toList();
if (heights.isEmpty()) return;
Set<Integer> levels = new java.util.LinkedHashSet<>();
for (double quantile : new double[]{.55, .35, .75, .15, .90}) {
int top = heights.get(Math.min(heights.size() - 1, (int) (heights.size() * quantile)));
for (int cut : new int[]{4, 8, 0}) levels.add(top - cut);
}
for (int level : levels) {
Set<Long> allowed = new HashSet<>();
for (int x = -168; x <= 168; x += 4) for (int z = -168; z <= 168; z += 4) {
if (!inside(x, z, 170)) continue;
boolean safe = riverRock(x, z, level);
for (int dx : new int[]{-3, 3}) for (int dz : new int[]{-3, 3}) {
if (safe && !riverRock(x + dx, z + dz, level)) safe = false;
}
if (safe) allowed.add(key(x, z));
}
for (int attempt = 0; attempt < 4 && allowed.size() > 100; attempt++) {
List<Point> route = longestRoute(allowed, level);
if (route.size() < 36) break;
Point first = route.getFirst(), last = route.getLast();
if (Math.hypot(last.x() - first.x(), last.z() - first.z()) < 100) break;
List<Point> path = smoothRoute(route);
Set<Long> wet = new HashSet<>();
Set<Long> bad = new HashSet<>();
for (Point at : path) {
int radius = field(at.x(), at.z(), 23, 8119) > .52 ? 3 : 2;
for (int dx = -radius - 1; dx <= radius + 1; dx++) for (int dz = -radius - 1; dz <= radius + 1; dz++) {
if (!riverRock(at.x() + dx, at.z() + dz, level)) {
for (int gx = -1; gx <= 1; gx++) for (int gz = -1; gz <= 1; gz++)
bad.add(key(Math.floorDiv(at.x(), 4) * 4 + gx * 4, Math.floorDiv(at.z(), 4) * 4 + gz * 4));
}
if (dx * dx + dz * dz <= radius * radius + 1) wet.add(key(at.x() + dx, at.z() + dz));
}
}
if (!bad.isEmpty()) { allowed.removeAll(bad); continue; }
// Both end pools are required and remain connected to the channel.
boolean pools = true;
for (Point endpoint : List.of(first, last)) {
Set<Long> pool = growRiverPool(endpoint, level);
if (pool.size() < 90) { pools = false; break; }
wet.addAll(pool);
}
if (!pools) { allowed.remove(key(first.x(), first.z())); allowed.remove(key(last.x(), last.z())); continue; }
long id = featureId(features.size());
Map<Long, Cell> river = new HashMap<>();
boolean safe = true;
for (long k : wet) {
Point at = point(k);
Column rock = column(at.x(), at.z());
int depth = 2;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !wet.contains(key(at.x() + d[0], at.z() + d[1]));
if (edge) depth = 1;
int bed = level - depth;
if (rock.top() < bed || rock.top() - bed > MAX_CARVE || rock.solidBottom() > bed - 6) { safe = false; break; }
river.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
wetMaterial(at.x(), at.z(), edge), id, 3));
}
if (!safe || !contained(river)) break;
addFeature(id, Kind.RIVER, level, path, river);
return;
}
}
}
private Set<Long> growRiverPool(Point center, int level) {
Set<Long> result = new HashSet<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
Set<Long> seen = new HashSet<>();
queue.add(new Node(center.x(), center.z(), 0));
while (!queue.isEmpty() && result.size() < 240) {
Node at = queue.remove();
if (!seen.add(key(at.x(), at.z())) || Math.hypot(at.x() - center.x(), at.z() - center.z()) > 12
|| !riverRock(at.x(), at.z(), level)) continue;
boolean retained = true;
for (int[] d : CARDINALS) retained &= solid(at.x() + d[0], level, at.z() + d[1]);
if (!retained) continue;
result.add(key(at.x(), at.z()));
for (int[] d : CARDINALS) {
int x = at.x() + d[0], z = at.z() + d[1];
queue.add(new Node(x, z, Math.hypot(x - center.x(), z - center.z())
+ field(x, z, 8, 411) * 6 + Math.max(0, column(x,z).top() - level) * .4));
}
}
return result;
}
private boolean riverRock(int x, int z, int level) {
Column rock = column(x, z);
return rock.top() >= level && rock.top() <= level + 12 && rock.solidBottom() <= level - 8;
}
private List<Point> longestRoute(Set<Long> allowed, int level) {
Set<Long> remaining = new HashSet<>(allowed);
List<Point> longest = List.of();
while (!remaining.isEmpty()) {
long start = remaining.stream().min(Long::compare).orElseThrow();
Map<Long, Long> first = routeTree(start, allowed);
remaining.removeAll(first.keySet());
if (first.size() < 36) continue;
long far = lastKey(first);
Map<Long, Long> second = routeTree(far, first.keySet());
long end = lastKey(second);
Map<Long, Long> shaped = riverRouteTree(far, end, first.keySet(), level);
List<Point> path = new ArrayList<>();
for (long at = end; ; at = shaped.get(at)) {
path.add(point(at));
if (at == far) break;
}
if (path.size() > longest.size()) longest = path;
}
return longest;
}
private Map<Long, Long> riverRouteTree(long start, long end, Set<Long> allowed, int level) {
Map<Long, Long> parent = new HashMap<>();
Map<Long, Double> costs = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
Point origin = point(start);
parent.put(start, start); costs.put(start, 0.0); queue.add(new Node(origin.x(), origin.z(), 0));
while (!queue.isEmpty()) {
Node at = queue.remove();
long current = key(at.x(), at.z());
if (at.cost() > costs.getOrDefault(current, Double.POSITIVE_INFINITY)) continue;
if (current == end) break;
for (int[] d : NEIGHBORS) {
int x = at.x() + d[0] * 4, z = at.z() + d[1] * 4;
long next = key(x, z);
if (!allowed.contains(next)) continue;
double cost = at.cost() + Math.hypot(d[0], d[1]) * (1 + Math.max(0, column(x,z).top() - level) * .14
+ field(x, z, 27, 8113) * 1.8);
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
parent.put(next, current); costs.put(next, cost); queue.add(new Node(x,z,cost));
}
}
return parent;
}
private List<Point> smoothRoute(List<Point> coarsePath) {
record Smooth(double x, double z) {}
List<Smooth> values = coarsePath.stream().map(p -> new Smooth(p.x(), p.z())).toList();
// Bend over several grid steps rather than rounding each voxel-sized corner.
for (int pass = 0; pass < 2; pass++) {
List<Smooth> next = new ArrayList<>();
for (int i = 0; i < values.size(); i++) {
if (i == 0 || i == values.size() - 1) { next.add(values.get(i)); continue; }
double x = 0, z = 0, sum = 0;
for (int offset = -4; offset <= 4; offset++) {
int weight = 5 - Math.abs(offset);
Smooth p = values.get(Math.clamp(i + offset, 0, values.size() - 1));
x += p.x() * weight; z += p.z() * weight; sum += weight;
}
next.add(new Smooth(x / sum, z / sum));
}
values = next;
}
for (int pass = 0; pass < 2; pass++) {
List<Smooth> next = new ArrayList<>(); next.add(values.getFirst());
for (int i = 1; i < values.size(); i++) {
Smooth a = values.get(i - 1), b = values.get(i);
next.add(new Smooth(a.x() * .75 + b.x() * .25, a.z() * .75 + b.z() * .25));
next.add(new Smooth(a.x() * .25 + b.x() * .75, a.z() * .25 + b.z() * .75));
}
next.add(values.getLast()); values = next;
}
List<Point> path = new ArrayList<>();
for (int i = 1; i < values.size(); i++) {
Smooth a = values.get(i - 1), b = values.get(i);
int steps = Math.max(1, (int) Math.ceil(Math.max(Math.abs(b.x() - a.x()), Math.abs(b.z() - a.z()))));
for (int step = 0; step <= steps; step++) {
double t = step / (double) steps;
Point p = new Point((int) Math.round(a.x() + (b.x() - a.x()) * t), (int) Math.round(a.z() + (b.z() - a.z()) * t));
if (path.isEmpty() || !path.getLast().equals(p)) path.add(p);
}
}
return path;
}
private Map<Long, Long> routeTree(long start, Set<Long> allowed) {
Map<Long, Long> parent = new LinkedHashMap<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
parent.put(start, start); queue.add(start);
while (!queue.isEmpty()) {
long current = queue.removeFirst();
Point at = point(current);
for (int[] d : NEIGHBORS) {
long next = key(at.x() + d[0] * 4, at.z() + d[1] * 4);
if (allowed.contains(next) && !parent.containsKey(next)) { parent.put(next, current); queue.addLast(next); }
}
}
return parent;
}
private long lastKey(Map<Long, Long> values) {
long result = 0;
for (long key : values.keySet()) result = key;
return result;
}
/** A real spring emerges from one of the river's taller rock banks and drops into the
* connected retained channel. The source replaces rock; its mouth is the carved valley. */
private void addRiverSpring() {
Feature river = features.stream().filter(f -> f.kind() == Kind.RIVER).findFirst().orElse(null);
if (river == null) return;
List<Cell> bank = cells.values().stream().filter(c -> c.featureId() == river.id())
.sorted(Comparator.comparingInt(Cell::carveTop).reversed().thenComparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
for (Cell wet : bank) for (int[] d : CARDINALS) {
int x = wet.x() + d[0], z = wet.z() + d[1];
if (cells.containsKey(key(x, z))) continue;
for (int height : new int[]{8, 7, 6, 5}) {
int y = wet.waterY() + height;
if (wet.carveTop() < y) continue;
Position source = new Position(x, y, z), outlet = new Position(wet.x(), y, wet.z());
boolean safe = true;
for (int sy = y - 3; sy <= y + 1; sy++) safe &= flowSolid(new Position(x, sy, z), outlet);
for (int[] side : CARDINALS) {
if (side[0] == -d[0] && side[1] == -d[1]) continue;
safe &= flowSolid(new Position(x + side[0], y, z + side[1]), outlet);
}
if (!safe) continue;
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.volume().stream().noneMatch(p -> {
Cell c = cells.get(key(p.x(), p.z()));
return c != null && c.featureId() == river.id() && p.y() <= c.waterY() && p.y() > c.bedY();
})) continue;
long id = mix(seed ^ 0xA81BE2L);
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
return;
}
}
}
private List<Candidate> candidates() {
for (int x = -RADIUS; x <= RADIUS; x += GRID) {
for (int z = -RADIUS; z <= RADIUS; z += GRID) {
if (inside(x, z, RADIUS)) coarse.put(key(x, z), coarseTop(x, z));
}
}
List<Candidate> result = new ArrayList<>();
for (int x = -RADIUS + GRID; x < RADIUS; x += GRID) {
for (int z = -RADIUS + GRID; z < RADIUS; z += GRID) {
int height = coarse.getOrDefault(key(x, z), -1);
if (height < 64 || !inside(x, z, RADIUS - 24)) continue;
double average = 0;
boolean safe = true;
for (int[] d : CARDINALS) {
int neighbor = coarse.getOrDefault(key(x + d[0] * GRID, z + d[1] * GRID), -1);
if (neighbor < 64 || Math.abs(neighbor - height) > 16) safe = false;
average += neighbor * 0.25;
}
if (safe) result.add(new Candidate(x, z,
(average - height) * 2.0 + random(seed + 2, x, z) * 3.0));
}
}
result.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z));
return result;
}
private int coarseTop(int x, int z) {
for (int y = 383; y >= 32; y -= 4) if (solid(x, y, z)) return y;
return -1;
}
private Column column(int x, int z) {
long key = key(x, z);
Column old = columns.get(key);
if (old != null) return old;
int top = -1;
// Exact scan, including everything above the planned water: a cave roof cannot be
// mistaken for a surface. Only candidates and their nearby shore need this refinement.
for (int y = 383; y >= 24; y--) {
if (solid(x, y, z)) { top = y; break; }
}
int bottom = top;
if (top >= 0) {
while (bottom > Math.max(16, top - 40) && solid(x, bottom - 1, z)) bottom--;
}
Column created = new Column(top, bottom);
columns.put(key, created);
return created;
}
private boolean eligible(int x, int z, int level) {
if (level < 32 || level >= 383 || !inside(x, z, RADIUS - 2)) return false;
Cell old = cells.get(key(x, z));
if (old != null && old.hasWater() && old.waterY() != level) return false;
Column column = column(x, z);
return column.top() >= level - 3 && column.top() < level + 6
&& column.solidBottom() <= Math.min(column.top(), level - 1) - 4;
}
private Set<Long> growBasin(Point center, int level, int target) {
if (!eligible(center.x(), center.z(), level)) return null;
Set<Long> wet = new HashSet<>();
Map<Long, Double> costs = new HashMap<>();
PriorityQueue<Node> queue = new PriorityQueue<>(NODE_ORDER);
queue.add(new Node(center.x(), center.z(), 0));
costs.put(key(center.x(), center.z()), 0.0);
while (!queue.isEmpty() && wet.size() < target) {
Node current = queue.remove();
if (current.cost() > costs.getOrDefault(key(current.x(), current.z()), Double.POSITIVE_INFINITY)) continue;
if (!eligible(current.x(), current.z(), level)) continue;
wet.add(key(current.x(), current.z()));
for (int[] d : NEIGHBORS) {
int x = current.x() + d[0], z = current.z() + d[1];
if (Math.hypot(x - center.x(), z - center.z()) > 40 || wet.contains(key(x, z))) continue;
if (!eligible(x, z, level)) continue;
double step = Math.hypot(d[0], d[1]);
double cost = current.cost() + step * (1 + Math.max(0, column(x, z).top() - level) * 0.8
+ terrainNoise(x, z) * 4.5 + field(x + 71, z - 93, 21, 107) * 3.5);
if (cost >= costs.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
costs.put(key(x, z), cost);
queue.add(new Node(x, z, cost));
}
}
wet = connectedTo(wet, key(center.x(), center.z()));
if (wet.size() < Math.min(target, 30)) return null;
// Filling a hollow requires every lower connected outlet to be included. Abort rather
// than place a dam if this natural basin runs to a cliff, cave or an oversized catchment.
List<Long> work = new ArrayList<>(wet);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
int x = point.x() + d[0], z = point.z() + d[1];
long neighbor = key(x, z);
if (wet.contains(neighbor) || column(x, z).top() >= level) continue;
if (wet.size() >= 1200 || Math.hypot(x - center.x(), z - center.z()) > 48
|| !eligible(x, z, level)) return null;
wet.add(neighbor);
work.add(neighbor);
}
}
return wet;
}
private Map<Long, Cell> waterCells(Set<Long> footprint, int level, long id) {
Map<Long, Cell> result = new HashMap<>();
for (long key : footprint) {
Point p = point(key);
if (!eligible(p.x(), p.z(), level)) return null;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !footprint.contains(key(p.x() + d[0], p.z() + d[1]));
Column column = column(p.x(), p.z());
int depth = Math.max(level - column.top() + 1, edge ? 1 : 2 + (terrainNoise(p.x(), p.z()) > 0.55 ? 1 : 0));
depth = Math.min(depth, 8 - Math.max(0, column.top() - level));
if (depth < 1 || depth > MAX_DEPTH) return null;
int bed = level - depth;
if (column.top() < bed || column.solidBottom() > bed - 2) return null;
ShoreMaterial material = wetMaterial(p.x(), p.z(), edge);
int sedimentDepth = sedimentDepth(p.x(), p.z());
if (column.solidBottom() > bed - sedimentDepth - 1) return null;
result.put(key, new Cell(p.x(), p.z(), level, bed, Math.max(level, column.top()),
material, id, sedimentDepth));
}
return result;
}
/** Deepen only sufficiently thick interiors, with one-block shore steps. Natural weak
* columns keep a shallower bed instead of receiving a manufactured lake foundation. */
private Map<Long, Cell> lakeCells(Set<Long> footprint, int level, long id) {
Map<Long, Integer> distance = new HashMap<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
for (long k : footprint) {
Point at = point(k);
for (int[] d : CARDINALS) if (!footprint.contains(key(at.x() + d[0], at.z() + d[1]))) {
distance.put(k, 0); queue.addLast(k); break;
}
}
while (!queue.isEmpty()) {
long k = queue.removeFirst(); Point at = point(k);
for (int[] d : CARDINALS) {
long next = key(at.x() + d[0], at.z() + d[1]);
if (footprint.contains(next) && !distance.containsKey(next)) {
distance.put(next, distance.get(k) + 1); queue.addLast(next);
}
}
}
Map<Long, Cell> result = new HashMap<>();
for (long k : footprint) {
Point at = point(k);
if (!eligible(at.x(), at.z(), level)) return null;
Column rock = column(at.x(), at.z());
int sediment = sedimentDepth(at.x(), at.z());
int wanted = Math.max(level - rock.top() + 1, Math.min(MAX_DEPTH, 1 + distance.get(k)));
int depth = Math.min(wanted, Math.min(level - rock.solidBottom() - sediment - 1,
MAX_LAKE_CARVE - Math.max(0, rock.top() - level)));
if (depth < 1 || rock.top() < level - depth) return null;
int bed = level - depth;
result.put(k, new Cell(at.x(), at.z(), level, bed, Math.max(level, rock.top()),
wetMaterial(at.x(), at.z(), distance.get(k) == 0), id, sediment));
}
return result;
}
private boolean contained(Map<Long, Cell> wet) {
for (Cell cell : wet.values()) {
for (int y = cell.bedY() - 2; y <= cell.bedY(); y++) {
if (!solid(cell.x(), y, cell.z())) return false;
}
for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
Cell neighbor = wet.get(key(x, z));
if (neighbor == null) neighbor = cells.get(key(x, z));
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
if (neighbor != null && neighbor.hasWater()) {
if (neighbor.waterY() != cell.waterY()) return false;
if (y > neighbor.bedY()) continue;
}
if (!solid(x, y, z)) return false;
}
}
}
return true;
}
private void addFeature(long id, Kind kind, int level, List<Point> path, Map<Long, Cell> wet) {
int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
int count = 0;
for (Cell cell : wet.values()) {
Cell old = cells.get(key(cell.x(), cell.z()));
if (old == null || !old.hasWater()) { cells.put(key(cell.x(), cell.z()), cell); count++; }
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
maxX = Math.max(maxX, cell.x()); maxZ = Math.max(maxZ, cell.z());
}
features.add(new Feature(id, kind, level, path, count, new Bounds(minX, minZ, maxX, maxZ)));
}
private static Set<Long> connectedTo(Set<Long> footprint, long start) {
Set<Long> connected = new HashSet<>();
List<Long> work = new ArrayList<>();
connected.add(start); work.add(start);
for (int i = 0; i < work.size(); i++) {
Point point = point(work.get(i));
for (int[] d : CARDINALS) {
long next = key(point.x() + d[0], point.z() + d[1]);
if (footprint.contains(next) && connected.add(next)) work.add(next);
}
}
return connected;
}
private record ShoreNode(int x, int z, int level, long feature, double cost) {}
/** Sediment follows shallow topography in broad, discontinuous sheets. A second nearby
* pocket can share the same sheet; the result is not a one-block material ring. */
private void addSedimentSheets() {
Comparator<ShoreNode> order = Comparator.comparingDouble(ShoreNode::cost)
.thenComparingInt(ShoreNode::x).thenComparingInt(ShoreNode::z).thenComparingLong(ShoreNode::feature);
Set<Long> rivers = features.stream().filter(f -> f.kind() == Kind.RIVER).map(Feature::id)
.collect(java.util.stream.Collectors.toSet());
PriorityQueue<ShoreNode> queue = new PriorityQueue<>(order);
Map<Long, Double> best = new HashMap<>();
for (Cell cell : cells.values()) if (cell.hasWater()) {
queue.add(new ShoreNode(cell.x(), cell.z(), cell.waterY(), cell.featureId(), 0));
best.put(key(cell.x(), cell.z()), 0.0);
}
while (!queue.isEmpty()) {
ShoreNode node = queue.remove();
if (node.cost() > best.getOrDefault(key(node.x(), node.z()), Double.POSITIVE_INFINITY)) continue;
boolean riverBank = rivers.contains(node.feature());
Column column = column(node.x(), node.z());
Cell existing = cells.get(key(node.x(), node.z()));
if (existing == null) {
boolean springRock = springs.stream().anyMatch(spring ->
Math.abs(spring.source().x() - node.x()) <= 4 && Math.abs(spring.source().z() - node.z()) <= 4);
if (springRock) continue;
double extent = riverBank ? 18 + field(node.x(), node.z(), 25, 131) * 6
: 9 + field(node.x(), node.z(), 25, 131) * 10;
if (node.cost() > extent) continue;
int delta = column.top() - node.level();
if (delta < 0 || delta > (riverBank ? 14 : 5)) continue;
// Low shore: waterline, then one-block steps. Cut at most two existing blocks;
// far deposits keep the original landform and feather back into grass/stone.
int terrace = node.level() + (int) Math.floor(node.cost() / (riverBank ? 2.5 : 4.5));
int cut = riverBank ? MAX_RIVER_BANK_CARVE : 2;
int bed = Math.max(node.level(), Math.max(column.top() - cut, Math.min(column.top(), terrace)));
int depth = sedimentDepth(node.x(), node.z());
if (column.solidBottom() > bed - depth - 1) continue;
boolean safe = true;
for (int[] d : CARDINALS) {
Cell next = cells.get(key(node.x() + d[0], node.z() + d[1]));
if (next != null && next.hasWater() && bed < next.waterY()) safe = false;
}
if (!safe) continue;
ShoreMaterial material = dryMaterial(node.x(), node.z(), node.cost() / extent);
cells.put(key(node.x(), node.z()), new Cell(node.x(), node.z(), -1, bed,
column.top(), material, node.feature(), depth));
}
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0], z = node.z() + d[1];
if (!inside(x, z, RADIUS - 2)) continue;
Cell water = cells.get(key(x, z));
if (water != null && water.hasWater()) continue;
Column next = column(x, z);
if (next.top() < node.level() || next.top() > node.level() + (riverBank ? 14 : 5)) continue;
double cost = node.cost() + Math.hypot(d[0], d[1])
* (0.7 + Math.abs(next.top() - column.top()) * 0.32 + field(x, z, 12, 211) * 0.6);
if (cost > (riverBank ? 24 : 19) || cost >= best.getOrDefault(key(x, z), Double.POSITIVE_INFINITY)) continue;
best.put(key(x, z), cost);
queue.add(new ShoreNode(x, z, node.level(), node.feature(), cost));
}
}
}
private int sedimentDepth(int x, int z) { return 3 + (int) (field(x, z, 15, 319) * 2.999); }
private ShoreMaterial wetMaterial(int x, int z, boolean edge) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
if (!edge && geology > 0.78) return ShoreMaterial.CLAY;
return ShoreMaterial.SAND;
}
private ShoreMaterial dryMaterial(int x, int z, double distance) {
double geology = field(x, z, 24, 503) * 0.75 + field(x, z, 7, 509) * 0.25;
double grass = field(x + 103, z - 137, 19, 521);
if (grass > 0.62 || distance > 0.60 + grass * 0.65) return ShoreMaterial.GRASS;
if (geology < 0.46) return ShoreMaterial.STONE;
if (geology < 0.65) return ShoreMaterial.GRAVEL;
return ShoreMaterial.SAND;
}
private record Cliff(int x, int z, int dx, int dz, double score) {}
private record FlowNode(Position position, int reach) {}
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
/** Pick rare exposed rock niches at the edges of real upper surfaces. The source replaces
* one rock block under an existing roof. Its outlet is already air over an eight-block drop;
* no source, shelf, dam, rock column or ocean is ever placed in empty terrain. */
private void addSprings() {
List<Cliff> cliffs = new ArrayList<>();
for (int x = -264; x <= 264; x += GRID) for (int z = -264; z <= 264; z += GRID) {
if (!inside(x, z, 264)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top < 88) continue;
for (int[] d : CARDINALS) {
int nx = x + d[0] * GRID, nz = z + d[1] * GRID;
int lower = coarse.computeIfAbsent(key(nx, nz), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
if (top - lower < 16) continue;
cliffs.add(new Cliff(x, z, d[0], d[1],
Math.min(64, top - lower) * 0.06 + random(seed + 811, x + d[0], z + d[1]) * 3));
}
}
cliffs.sort(Comparator.comparingDouble(Cliff::score).reversed()
.thenComparingInt(Cliff::x).thenComparingInt(Cliff::z)
.thenComparingInt(Cliff::dx).thenComparingInt(Cliff::dz));
int attempts = 0;
for (Cliff cliff : cliffs) {
if (springs.size() >= 3 || attempts++ >= 160) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2, -4, 4}) {
for (int along = 0; along < GRID; along++) {
int x = cliff.x() + cliff.dx() * along - cliff.dz() * tangent;
int z = cliff.z() + cliff.dz() * along + cliff.dx() * tangent;
if (!inside(x, z, 266)) continue;
int ox = x + cliff.dx(), oz = z + cliff.dz();
Column rock = column(x, z), outletColumn = column(ox, oz);
if (rock.top() < 80 || rock.top() - outletColumn.top() < 14) continue;
// Five blocks below the highest natural surface is below the soil cap.
int floor = Math.max(64, Math.max(rock.top() - 20, outletColumn.top() + 8));
for (int y = rock.top() - 5; y >= floor; y -= 3) {
Position source = new Position(x, y, z), outlet = new Position(ox, y, oz);
if (!separatedSpring(source) || !springNiche(source, cliff.dx(), cliff.dz()) || nearModifiedGround(source, 32)) continue;
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean nearModifiedGround(Position source, int distance) {
for (Cell cell : cells.values()) {
int dy = Math.max(Math.max(cell.bedY() - cell.sedimentDepth() - source.y(), 0),
source.y() - cell.carveTop());
long dx = source.x() - cell.x(), dz = source.z() - cell.z();
if (dx * dx + dz * dz + (long) dy * dy < (long) distance * distance) return true;
}
return false;
}
private boolean separatedSpring(Position source) {
for (Spring spring : springs) {
double horizontal = Math.hypot(source.x() - spring.source().x(), source.z() - spring.source().z());
int dy = Math.abs(source.y() - spring.source().y());
if (Math.hypot(horizontal, dy) < 64 || dy < 12) return false;
}
return true;
}
private record Wall(int x, int y, int z, int dx, int dz, double score) {}
/** Probe a few exposed faces below the highest surface as well. This finds overhangs and
* lower strata that a highest-column heightmap cannot represent, without inventing a ledge. */
private void addLowerWallSprings() {
List<Wall> candidates = new ArrayList<>();
for (int x = -240; x <= 240; x += 16) for (int z = -240; z <= 240; z += 16) {
if (!inside(x, z, 240)) continue;
int top = coarse.computeIfAbsent(key(x, z), ignored -> coarseTop(point(ignored).x(), point(ignored).z()));
for (int y = top - 32; y >= 64; y -= 16) {
if (!solid(x, y, z)) continue;
for (int[] direction : CARDINALS) {
if (solid(x + direction[0] * 4, y, z + direction[1] * 4)) continue;
candidates.add(new Wall(x, y, z, direction[0], direction[1],
random(seed + 1217 + y, x + direction[0], z + direction[1]) * 3
+ Math.min(100, top - y) * 0.015));
}
}
}
candidates.sort(Comparator.comparingDouble(Wall::score).reversed()
.thenComparingInt(Wall::x).thenComparingInt(Wall::z).thenComparingInt(Wall::y)
.thenComparingInt(Wall::dx).thenComparingInt(Wall::dz));
int attempts = 0;
for (Wall candidate : candidates) {
if (springs.size() >= 2 || attempts++ >= 96) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, 3, -3, 6, -6}) {
int y = candidate.y() + dy;
if (y < 56 || column(x, z).top() - y < 24) continue;
Position source = new Position(x, y, z);
if (!separatedSpring(source) || !springNiche(source, candidate.dx(), candidate.dz())
|| nearModifiedGround(source, 32)) continue;
Position outlet = new Position(x + candidate.dx(), y, z + candidate.dz());
Flow flow = traceSpring(source, outlet);
if (flow == null || flow.bounds().minY() > y - 12) continue;
long id = mix(seed ^ (0x5F81A6L + springs.size() * 0x9E3779B97F4A7C15L));
springs.add(new Spring(id, source, outlet, flow.path(), flow.bounds()));
springFlow.addAll(flow.volume());
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
}
private boolean springNiche(Position source, int dx, int dz) {
for (int y = source.y() - 3; y <= source.y() + 1; y++) {
if (!solid(source.x(), y, source.z())) return false;
}
for (int[] d : CARDINALS) {
if (d[0] == dx && d[1] == dz) continue;
if (!solid(source.x() + d[0], source.y(), source.z() + d[1])) return false;
}
for (int y = source.y(); y >= source.y() - 8; y--) {
if (solid(source.x() + dx, y, source.z() + dz)) return false;
}
return true;
}
/** Predict drainage through undecorated rock to select springs and inspection bounds.
* Descend through air first, then explore seven horizontal steps on a shelf. Later trees
* can divert the actual fluid: this prediction is not a bound on decorated-world physics.
* Integration tests trace actual water back to its source within the inspected chunk halo. */
private Flow traceSpring(Position source, Position outlet) {
Map<Position, Integer> reach = new HashMap<>();
Map<Position, Position> parent = new HashMap<>();
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
reach.put(source, 7);
queue.add(new FlowNode(source, 7));
Position lowest = source;
while (!queue.isEmpty()) {
FlowNode node = queue.removeFirst();
Position at = node.position();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 24000) return null;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (!flowSolid(below, source)) {
enqueueFlow(below, 7, at, queue, reach, parent);
continue;
}
if (node.reach() <= 0) continue;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (flowSolid(next, source)) continue;
enqueueFlow(next, node.reach() - 1, at, queue, reach, parent);
}
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY;
int minZ = source.z(), maxZ = minZ;
for (Position position : reach.keySet()) {
minX = Math.min(minX, position.x()); maxX = Math.max(maxX, position.x());
minY = Math.min(minY, position.y()); maxY = Math.max(maxY, position.y());
minZ = Math.min(minZ, position.z()); maxZ = Math.max(maxZ, position.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Flow(Set.copyOf(reach.keySet()), path, new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Position source) {
if (at.equals(source)) return false;
Cell cell = cells.get(key(at.x(), at.z()));
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
return solid(at.x(), at.y(), at.z());
}
private static void enqueueFlow(Position next, int available, Position from, ArrayDeque<FlowNode> queue,
Map<Position, Integer> reach, Map<Position, Position> parent) {
if (available <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, available);
queue.addLast(new FlowNode(next, available));
}
private boolean nearWater(int x, int z, int distance) {
for (Feature feature : features) {
Point point = feature.path().getFirst();
if (Math.hypot(x - point.x(), z - point.z()) < distance) return true;
}
return false;
}
private boolean solid(int x, int y, int z) { samples++; return sampler.sample(x, y, z) > 0; }
private long featureId(int index) { return mix(seed ^ (0x51A7E7L + index * 0x9E3779B97F4A7C15L)); }
private double terrainNoise(int x, int z) { return field(x, z, 8, 31); }
private double field(int x, int z, int scale, long salt) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
double a = random(seed + salt, gx, gz) * (1 - tx) + random(seed + salt, gx + 1, gz) * tx;
double b = random(seed + salt, gx, gz + 1) * (1 - tx) + random(seed + salt, gx + 1, gz + 1) * tx;
return a * (1 - tz) + b * tz;
}
}
private static boolean inside(int x, int z, int radius) { return Math.hypot((double) x, z) < radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static Point point(long key) { return new Point((int) (key >> 32), (int) key); }
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,191 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
/** Alpha.9 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class CavernHydrologyRuntime {
private static final Map<RandomState, CavernHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, CavernLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private CavernHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.CAVERN_SETTINGS);
}
public static CavernHydrology.Plan cavernPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static CavernHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Cavern hydrology is not enabled for this generator");
return PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var plan = CavernHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary cavern hydrology: seed {}, {} features, {} columns, {} springs, {} ms",
state.seed(), plan.features().size(), plan.cells().size(), plan.springs().size(),
(System.nanoTime() - start) / 1_000_000);
SanctuaryMod.LOGGER.info("Sanctuary cavern catchments: {}", plan.features().stream().map(f -> f.kind() + " Y" + f.waterY() + " water=" + f.waterCells() + " path=" + f.path().size() + " " + f.bounds()).toList());
return plan;
});
}
public static CavernLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static CavernLavaDeposit.Plan lavaPlan(NoiseBasedChunkGenerator generator, RandomState randomState) {
// Acquire the water plan before the lava cache lock; the water planner never acquires it.
var water = plan(generator, randomState);
return LAVA_PLANS.computeIfAbsent(randomState, state -> {
long start = System.nanoTime();
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
var lava = CavernLavaDeposit.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z), water);
SanctuaryMod.LOGGER.info("Sanctuary starter lava: seed {}, {} sources, access {}, {} ms",
state.seed(), lava.lavaCells().size(), lava.access(), (System.nanoTime() - start) / 1_000_000);
return lava;
});
}
public static void apply(NoiseBasedChunkGenerator generator, RandomState randomState, ChunkAccess chunk) {
if (!enabled(generator)) return;
var chunkPos = chunk.getPos();
if (Math.abs((long) chunkPos.x() * 16) > IslandShape.TERRAIN_LIMIT + 16L
|| Math.abs((long) chunkPos.z() * 16) > IslandShape.TERRAIN_LIMIT + 16L) return;
var plan = plan(generator, randomState);
var cells = plan.cellsInChunk(chunkPos.x(), chunkPos.z());
var springs = plan.springsInChunk(chunkPos.x(), chunkPos.z());
var outlets = new java.util.ArrayList<>(plan.springs().stream().map(CavernHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
for (var spill : plan.spills()) {
var outlet = spill.outlet();
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()) outlets.add(outlet);
}
var lava = lavaPlan(generator, randomState);
for (var fall : lava.falls()) {
var outlet = fall.outlet();
if ((outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z())
outlets.add(new CavernHydrology.Position(outlet.x(), outlet.y(), outlet.z()));
}
var lavaCells = lava.cellsInChunk(chunkPos.x(), chunkPos.z());
var lavaSupports = lava.supportsInChunk(chunkPos.x(), chunkPos.z());
if (cells.isEmpty() && springs.isEmpty() && outlets.isEmpty() && lavaCells.isEmpty() && lavaSupports.isEmpty()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
// Validate all replacements and two intact support layers before modifying this chunk.
for (var cell : cells) {
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++) {
requireSolid(chunk, pos.set(cell.x(), y, cell.z()), randomState.seed());
}
}
for (var spring : springs) {
var source = spring.source();
requireSolid(chunk, pos.set(source.x(), source.y(), source.z()), randomState.seed());
}
for (var support : lavaSupports) {
requireSolid(chunk, pos.set(support.x(), support.y(), support.z()), randomState.seed());
if (chunk.getBlockState(pos).ignitedByLava()) {
throw new IllegalStateException("Lava pocket requires nonflammable natural support at " + pos);
}
}
for (var cell : lavaCells) {
requireSolid(chunk, pos.set(cell.x(), cell.y(), cell.z()), randomState.seed());
}
for (var cell : cells) {
for (int depth = 0; depth < cell.sedimentDepth(); depth++) {
pos.set(cell.x(), cell.bedY() - depth, cell.z());
BlockState material = CavernMaterials.sedimentBlock(cell, randomState.seed(), depth);
chunk.setBlockState(pos, material, 0);
}
for (int y = cell.bedY() + 1; y <= Math.max(cell.waterY(), cell.carveTop()); y++) {
pos.set(cell.x(), y, cell.z());
chunk.setBlockState(pos, cell.hasWater() && y <= cell.waterY()
? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState(), 0);
if (cell.hasWater() && y <= cell.waterY()) chunk.markPosForPostProcessing(pos);
}
}
for (var spring : springs) {
var source = spring.source();
pos.set(source.x(), source.y(), source.z());
chunk.setBlockState(pos, Blocks.WATER.defaultBlockState(), 0);
// Only the source is generated. Vanilla fluid ticks create the descending waterfall.
chunk.markPosForPostProcessing(pos);
}
for (var cell : lavaCells) {
pos.set(cell.x(), cell.y(), cell.z());
chunk.setBlockState(pos, (cell.lava() ? Blocks.LAVA : Blocks.AIR).defaultBlockState(), 0);
if (cell.lava()) chunk.markPosForPostProcessing(pos);
}
// This existing vanilla queue is consumed after neighbouring chunks finish decoration.
// Mark the outlet in its own chunk, including when its source belongs to a neighbour.
for (var outlet : outlets) {
pos.set(outlet.x(), outlet.y(), outlet.z());
chunk.markPosForPostProcessing(pos);
}
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG));
}
/** Reserve water, sediment and hot-rock volumes before placing vegetation. */
public static boolean protects(NoiseBasedChunkGenerator generator, RandomState randomState, BlockPos base,
int radius, int height, int rootDepth) {
var water = plan(generator, randomState);
var lava = lavaPlan(generator, randomState);
int low = base.getY() - rootDepth, high = base.getY() + height;
for (int x = base.getX() - radius; x <= base.getX() + radius; x++) {
for (int z = base.getZ() - radius; z <= base.getZ() + radius; z++) {
for (var cell : water.cellsAt(x, z))
if (low <= cell.carveTop() && high >= cell.bedY() - cell.sedimentDepth() - 1) return true;
}
}
for (var spring : water.springs()) {
var bounds = spring.flowBounds();
if (base.getX() + radius >= bounds.minX() - 2 && base.getX() - radius <= bounds.maxX() + 2
&& base.getZ() + radius >= bounds.minZ() - 2 && base.getZ() - radius <= bounds.maxZ() + 2
&& low <= bounds.maxY() + 2 && high >= bounds.minY() - 2) return true;
var source = spring.source();
if (Math.abs(base.getX() - source.x()) <= radius + 3 && Math.abs(base.getZ() - source.z()) <= radius + 3
&& low <= source.y() + 3 && high >= source.y() - 3) return true;
}
for (var bounds : lava.flowBounds()) {
if (base.getX() + radius >= bounds.minX() - 8 && base.getX() - radius <= bounds.maxX() + 8
&& base.getZ() + radius >= bounds.minZ() - 8 && base.getZ() - radius <= bounds.maxZ() + 8
&& low <= bounds.maxY() + 8 && high >= bounds.minY() - 8) return true;
}
for (var spill : water.spills()) {
var bounds = spill.flowBounds();
if (base.getX() + radius >= bounds.minX() - 3 && base.getX() - radius <= bounds.maxX() + 3
&& base.getZ() + radius >= bounds.minZ() - 3 && base.getZ() - radius <= bounds.maxZ() + 3
&& low <= bounds.maxY() + 3 && high >= bounds.minY() - 3) return true;
}
for (var cell : lava.cells()) if (Math.abs(base.getX() - cell.x()) <= radius + 2 && Math.abs(base.getZ() - cell.z()) <= radius + 2
&& low <= cell.y() + 2 && high >= cell.y() - 2) return true;
for (var support : lava.supports()) if (Math.abs(base.getX() - support.x()) <= radius + 2 && Math.abs(base.getZ() - support.z()) <= radius + 2
&& low <= support.y() + 2 && high >= support.y() - 2) return true;
return false;
}
private static void requireSolid(ChunkAccess chunk, BlockPos pos, long seed) {
if (!chunk.getBlockState(pos).isCollisionShapeFullBlock(chunk, pos)) {
throw new IllegalStateException("Cavern hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -0,0 +1,99 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.stream.Stream;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeResolver;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
/** Seeded alpha.9 rift climate, separate from all saved earlier biome-source codecs. */
public final class CavernIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> OAK_FOREST = key("cavern_oak_forest");
public static final ResourceKey<Biome> BIRCH_FOREST = key("cavern_birch_forest");
public static final ResourceKey<Biome> CLEARING = key("cavern_clearing");
public static final ResourceKey<Biome> DRY_WOODLAND = key("cavern_dry_woodland");
public static final ResourceKey<Biome> ROCKY_HEATH = key("cavern_rocky_heath");
public static final ResourceKey<Biome> DARK_GROVE = key("cavern_dark_grove");
public static final ResourceKey<Biome> BAMBOO_GROVE = key("cavern_bamboo_grove");
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("cavern_sulfur_depths");
public static final ResourceKey<Biome> LUSH_CAVES = key("cavern_lush_caves");
public static final ResourceKey<Biome> DRIPSTONE_CAVES = key("cavern_dripstone_caves");
public static final MapCodec<CavernIslandBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Biome.CODEC.fieldOf("oak_forest").forGetter(source -> source.oakForest),
Biome.CODEC.fieldOf("birch_forest").forGetter(source -> source.birchForest),
Biome.CODEC.fieldOf("clearing").forGetter(source -> source.clearing),
Biome.CODEC.fieldOf("dry_woodland").forGetter(source -> source.dryWoodland),
Biome.CODEC.fieldOf("rocky_heath").forGetter(source -> source.rockyHeath),
Biome.CODEC.fieldOf("dark_grove").forGetter(source -> source.darkGrove),
Biome.CODEC.fieldOf("bamboo_grove").forGetter(source -> source.bambooGrove),
Biome.CODEC.fieldOf("sulfur_depths").forGetter(source -> source.sulfurDepths),
Biome.CODEC.fieldOf("lush_caves").forGetter(source -> source.lushCaves),
Biome.CODEC.fieldOf("dripstone_caves").forGetter(source -> source.dripstoneCaves)
).apply(instance, CavernIslandBiomeSource::new));
private final Holder<Biome> oakForest;
private final Holder<Biome> birchForest;
private final Holder<Biome> clearing;
private final Holder<Biome> dryWoodland;
private final Holder<Biome> rockyHeath;
private final Holder<Biome> darkGrove;
private final Holder<Biome> bambooGrove;
private final Holder<Biome> sulfurDepths;
private final Holder<Biome> lushCaves;
private final Holder<Biome> dripstoneCaves;
public CavernIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths, Holder<Biome> lushCaves, Holder<Biome> dripstoneCaves) {
this.oakForest = oakForest;
this.birchForest = birchForest;
this.clearing = clearing;
this.dryWoodland = dryWoodland;
this.rockyHeath = rockyHeath;
this.darkGrove = darkGrove;
this.bambooGrove = bambooGrove;
this.sulfurDepths = sulfurDepths;
this.lushCaves = lushCaves;
this.dripstoneCaves = dripstoneCaves;
}
@Override
protected MapCodec<CavernIslandBiomeSource> codec() { return CODEC; }
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths, lushCaves, dripstoneCaves);
}
@Override
public BiomeResolver createResolver(Climate.Sampler sampler) {
return (quartX, quartY, quartZ) -> {
int x = quartX * 4, y = quartY * 4, z = quartZ * 4;
float variation = sampler.temperature().sampleValue(x, 0, z);
float moisture = sampler.humidity().sampleValue(x, 0, z);
return switch (CavernBiomePalette.select(y, variation, moisture)) {
case OAK_FOREST -> oakForest;
case BIRCH_FOREST -> birchForest;
case CLEARING -> clearing;
case DRY_WOODLAND -> dryWoodland;
case ROCKY_HEATH -> rockyHeath;
case DARK_GROVE -> darkGrove;
case BAMBOO_GROVE -> bambooGrove;
case SULFUR_DEPTHS -> sulfurDepths;
case LUSH_CAVES -> lushCaves;
case DRIPSTONE_CAVES -> dripstoneCaves;
};
};
}
private static ResourceKey<Biome> key(String path) {
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
}
}
@@ -0,0 +1,331 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** Alpha.9 lava: an optional covered pocket and up to two deep rock outlets.
* All sources replace natural rock; Minecraft alone produces the falling lava. */
public final class CavernLavaDeposit {
public static final int RADIUS = 192;
public static final int WATER_CLEARANCE = 32;
public static final int LAVA_VOLUME = 18;
private static final int ENTRY_DISTANCE = 8;
private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
.thenComparingInt(Position::z).thenComparingInt(Position::y);
private static final Comparator<Cell> CELL_ORDER = Comparator.comparingInt(Cell::x)
.thenComparingInt(Cell::z).thenComparingInt(Cell::y);
private CavernLavaDeposit() {}
public record Position(int x, int y, int z) {}
/** lava=false removes existing rock to open the two-block-high chamber or approach. */
public record Cell(int x, int y, int z, boolean lava) {}
public static final class Plan {
private final List<Cell> cells;
private final List<Cell> lavaCells;
private final List<Position> supports;
private final Optional<Position> access;
private final Map<Long, List<Cell>> chunks;
private final Map<Long, List<Position>> supportChunks;
private final List<CavernLavaFlows.Fall> falls;
private final Map<Long, List<CavernLavaFlows.Fall>> fallChunks;
private Plan(List<Cell> cells, Set<Position> supports, Position access) {
this(cells, supports, access, List.of());
}
private Plan(List<Cell> cells, Set<Position> supports, Position access, List<CavernLavaFlows.Fall> falls) {
this.falls = List.copyOf(falls);
Map<Long, List<CavernLavaFlows.Fall>> groupedFalls = new HashMap<>();
for (var fall : falls) groupedFalls.computeIfAbsent(
key(fall.source().x() >> 4, fall.source().z() >> 4), ignored -> new ArrayList<>()).add(fall);
groupedFalls.replaceAll((ignored, values) -> List.copyOf(values));
fallChunks = Map.copyOf(groupedFalls);
this.cells = cells.stream().sorted(CELL_ORDER).toList();
lavaCells = this.cells.stream().filter(Cell::lava).toList();
this.supports = supports.stream().sorted(POSITION_ORDER).toList();
this.access = Optional.ofNullable(access);
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : this.cells) byChunk.computeIfAbsent(key(cell.x() >> 4, cell.z() >> 4),
ignored -> new ArrayList<>()).add(cell);
byChunk.replaceAll((ignored, values) -> List.copyOf(values));
chunks = Map.copyOf(byChunk);
Map<Long, List<Position>> bySupportChunk = new HashMap<>();
for (Position support : this.supports) bySupportChunk.computeIfAbsent(key(support.x() >> 4, support.z() >> 4),
ignored -> new ArrayList<>()).add(support);
bySupportChunk.replaceAll((ignored, values) -> List.copyOf(values));
supportChunks = Map.copyOf(bySupportChunk);
}
public List<Cell> cells() { return cells; }
public List<Cell> lavaCells() { return lavaCells; }
public List<Position> supports() { return supports; }
public List<CavernLavaFlows.Fall> falls() { return falls; }
public List<CavernLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
public List<Position> flowSources() { return falls.stream().map(CavernLavaFlows.Fall::source).toList(); }
public List<CavernHydrology.FlowBounds> flowBounds() { return falls.stream().map(CavernLavaFlows.Fall::flowBounds).toList(); }
/** Player feet on a natural ledge, with three blocks of headroom below a roof or sky. */
public Optional<Position> access() { return access; }
public List<Cell> cellsInChunk(int x, int z) { return chunks.getOrDefault(key(x, z), List.of()); }
public List<Position> supportsInChunk(int x, int z) { return supportChunks.getOrDefault(key(x, z), List.of()); }
}
public static Plan create(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
Plan pocket = new Search(seed, sampler, hydrology).find();
List<CavernLavaFlows.Fall> falls = CavernLavaFlows.create(seed, sampler, hydrology, pocket);
List<Cell> cells = new ArrayList<>(pocket.cells());
Set<Position> supports = new HashSet<>(pocket.supports());
for (var fall : falls) {
Position source = fall.source();
cells.add(new Cell(source.x(), source.y(), source.z(), true));
supports.addAll(fall.supports());
}
return new Plan(cells, supports, pocket.access().orElse(null), falls);
}
private record Candidate(int x, int z, int dx, int dz, double score) {}
private record LowerCandidate(int x, int y, int z, int dx, int dz, double score) {}
private static final class Search {
private final long seed;
private final CavernHydrology.Sampler sampler;
private final CavernHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan hydrology) {
this.seed = seed;
this.sampler = sampler;
this.hydrology = hydrology;
}
Plan find() {
Plan lower = findLowerPocket();
if (lower != null) return lower;
List<Candidate> candidates = new ArrayList<>();
for (int x = -184; x <= 184; x += 8) for (int z = -184; z <= 184; z += 8) {
if (!inside(x, z, RADIUS - 8)) continue;
int ledge = top(x, z);
if (ledge < 64 || ledge > 300) continue;
for (int[] direction : DIRECTIONS) {
int cx = x - direction[0] * ENTRY_DISTANCE, cz = z - direction[1] * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || !clearOfWater(cx, cz)) continue;
int rise = top(cx, cz) - ledge;
if (rise < 12) continue;
candidates.add(new Candidate(x, z, direction[0], direction[1],
random(seed, cx, cz) + Math.min(rise, 40) * 0.015));
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (attempts++ >= 192) break;
// Refine the coarse edge, including a ledge lying between two eight-block samples.
for (int along = 0; along < 8; along++) for (int tangent : new int[]{0, -3, 3}) {
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz());
if (plan != null) return plan;
}
}
return new Plan(List.of(), Set.of(), null);
}
/** Prefer a real lower ledge below an overhang. The chamber uses exactly the same rock
* shell as the outdoor fallback; only the entrance may now have a natural ceiling. */
private Plan findLowerPocket() {
List<LowerCandidate> candidates = new ArrayList<>();
for (int x = -176; x <= 176; x += 16) for (int z = -176; z <= 176; z += 16) {
if (!inside(x, z, RADIUS - 8)) continue;
for (int coarseY = 64; coarseY <= 160; coarseY += 8) {
if (!solid(x, coarseY, z) || solid(x, coarseY + 8, z)) continue;
int level = coarseY;
for (int y = coarseY + 7; y > coarseY; y--) {
if (solid(x, y, z)) { level = y; break; }
}
if (level > 160 || top(x, z) < level + 12) continue;
for (int[] direction : DIRECTIONS) {
int inwardX = x - direction[0] * 16, inwardZ = z - direction[1] * 16;
if (!inside(inwardX, inwardZ, RADIUS - 12)
|| !solid(inwardX, level + 2, inwardZ) || !solid(inwardX, level + 10, inwardZ)) continue;
candidates.add(new LowerCandidate(x, level, z, direction[0], direction[1],
random(seed + level + 0x108E2L, x - direction[0], z - direction[1])
+ (160 - level) * .002));
}
}
}
candidates.sort(Comparator.comparingDouble(LowerCandidate::score).reversed()
.thenComparingInt(LowerCandidate::x).thenComparingInt(LowerCandidate::z)
.thenComparingInt(LowerCandidate::y).thenComparingInt(LowerCandidate::dx).thenComparingInt(LowerCandidate::dz));
int attempts = 0;
for (LowerCandidate candidate : candidates) {
if (attempts++ >= 128) break;
for (int along = 0; along < 16; along++) for (int tangent : new int[]{0, -3, 3}) {
int x = candidate.x() - candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() - candidate.dz() * along + candidate.dx() * tangent;
for (int level = Math.min(160, candidate.y() + 5); level >= Math.max(64, candidate.y() - 5); level--) {
// Reject solid cave walls and missing floors before expensive shell probes.
if (!solid(x, level, z) || solid(x, level + 1, z)) continue;
Plan plan = tryNiche(x, z, candidate.dx(), candidate.dz(), level, true);
if (plan != null) return plan;
}
}
}
return null;
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz) {
return tryNiche(entryX, entryZ, dx, dz, top(entryX, entryZ), false);
}
private Plan tryNiche(int entryX, int entryZ, int dx, int dz, int level, boolean underground) {
int cx = entryX - dx * ENTRY_DISTANCE, cz = entryZ - dz * ENTRY_DISTANCE;
if (!inside(cx, cz, RADIUS - 12) || level < 64 || level > (underground ? 160 : 300)
|| top(cx, cz) < level + 12) return null;
// A lower entrance has genuine overhead island rock, but does not need a sky view.
if (underground && top(entryX, entryZ) < level + 12) return null;
int airLimit = underground ? level + 3 : 383;
for (int y = level + 1; y <= airLimit; y++) if (solid(entryX, y, entryZ)) return null;
for (int y = level - 2; y <= level; y++) if (!solid(entryX, y, entryZ)) return null;
if (underground ? !clearOfWater(cx, level, cz) : !clearOfWater(cx, cz)) return null;
List<Cell> cells = new ArrayList<>();
Set<Position> changed = new HashSet<>();
Set<Position> supports = new HashSet<>();
for (int x = cx - 1; x <= cx + 1; x++) for (int z = cz - 1; z <= cz + 1; z++) {
for (int y = level - 1; y <= level + 2; y++) {
if (!solid(x, y, z)) return null;
add(cells, changed, x, y, z, y <= level);
}
}
// A dry lip separates the ledge from the pool. Only the air above it is opened.
for (int distance = 2; distance < ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level + 1; y <= level + 2; y++) {
if (solid(x, y, z)) add(cells, changed, x, y, z, false);
}
}
// Every carved column must lie under eight continuous natural rock blocks. This
// excludes soil-cap excavation at the entrance as well as exposed surface lava.
Map<Long, Integer> highest = new HashMap<>();
for (Cell cell : cells) highest.merge(key(cell.x(), cell.z()), cell.y(), Math::max);
for (var column : highest.entrySet()) {
int x = (int) (column.getKey() >> 32), z = (int) (long) column.getKey();
for (int y = column.getValue() + 1; y <= column.getValue() + 8; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
// Three intact blocks under the pool, two-block-thick side walls, and a solid chamber
// surround prevent escape through an adjacent cave. No part of this shell is created.
for (int x = cx - 3; x <= cx + 3; x++) for (int z = cz - 3; z <= cz + 3; z++) {
for (int y = level - 4; y <= level + 2; y++) {
if (changed.contains(new Position(x, y, z))) continue;
if (!requireRock(supports, x, y, z)) return null;
}
}
// The natural roof also covers a conservative envelope around local lava fire spread.
// Its thickness keeps later surface vegetation well above the lava chamber.
for (int x = cx - 4; x <= cx + 4; x++) for (int z = cz - 4; z <= cz + 4; z++) {
for (int y = level + 3; y <= level + 10; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
for (int distance = 2; distance <= ENTRY_DISTANCE; distance++) {
int x = cx + dx * distance, z = cz + dz * distance;
for (int y = level - 2; y <= level; y++) {
if (!requireRock(supports, x, y, z)) return null;
}
}
supports.removeAll(changed);
return new Plan(cells, supports, new Position(entryX, level + 1, entryZ));
}
private boolean requireRock(Set<Position> supports, int x, int y, int z) {
if (!solid(x, y, z)) return false;
supports.add(new Position(x, y, z));
return true;
}
private boolean clearOfWater(int x, int z) {
// The extra ten blocks include both the entrance and the complete protected shell.
int clearance = WATER_CLEARANCE + 10;
for (var cell : hydrology.cells()) {
long dx = x - cell.x(), dz = z - cell.z();
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
for (var spring : hydrology.springs()) {
var bounds = spring.flowBounds();
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
for (var spill : hydrology.spills()) {
var bounds = spill.flowBounds();
long dx = Math.max(Math.max((long) bounds.minX() - x, 0), (long) x - bounds.maxX());
long dz = Math.max(Math.max((long) bounds.minZ() - z, 0), (long) z - bounds.maxZ());
if (dx * dx + dz * dz < (long) clearance * clearance) return false;
}
return true;
}
private boolean clearOfWater(int x, int level, int z) {
// This box covers chamber, dry approach, intact shell and roof. A lake far above is
// allowed; an actual waterfall crossing this level still excludes the entire pocket.
var pocket = new CavernHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
for (var cell : hydrology.cells()) {
var ground = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
if (distanceSquared(pocket, ground) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spring : hydrology.springs()) {
if (distanceSquared(pocket, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spill : hydrology.spills()) {
if (distanceSquared(pocket, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
return true;
}
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
return dx * dx + dy * dy + dz * dz;
}
private int top(int x, int z) {
return heights.computeIfAbsent(key(x, z), ignored -> {
for (int y = 383; y >= 32; y -= 4) {
if (!solid(x, y, z)) continue;
for (int exact = Math.min(383, y + 3); exact > y; exact--) if (solid(x, exact, z)) return exact;
return y;
}
return -1;
});
}
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
}
private static void add(List<Cell> cells, Set<Position> changed, int x, int y, int z, boolean lava) {
if (changed.add(new Position(x, y, z))) cells.add(new Cell(x, y, z, lava));
}
private static boolean inside(int x, int z, int radius) { return (long) x * x + (long) z * z < (long) radius * radius; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static double random(long seed, int x, int z) {
long value = seed ^ key(x, z) ^ 0x1A7A5EEDL;
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return ((value ^ (value >>> 31)) >>> 11) * 0x1.0p-53;
}
}
@@ -0,0 +1,241 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.CavernLavaDeposit.Position;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Selects a few volcanic seeps in deep exposed rock. Sources replace rock; their outlets and
* entire falling columns are left empty for vanilla lava ticks. No retaining wall is generated. */
public final class CavernLavaFlows {
public static final int MIN_SOURCE_Y = 40;
public static final int MAX_SOURCE_Y = 160;
public static final int MAX_FALLS = 2;
public static final int HORIZONTAL_REACH = 3;
public static final int FIRE_CLEARANCE = 8;
public static final int WATER_CLEARANCE = 24;
private static final int RADIUS = 224;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Comparator<Position> POSITION_ORDER = Comparator.comparingInt(Position::x)
.thenComparingInt(Position::z).thenComparingInt(Position::y);
private CavernLavaFlows() {}
/** flowBounds predicts undecorated rock drainage; it does not override Minecraft physics.
* supports contains only unchanged natural rock around the source, never the source itself. */
public record Fall(long id, Position source, Position outlet, List<Position> flowPath,
CavernHydrology.FlowBounds flowBounds, List<Position> supports) {
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
}
public static List<Fall> create(long seed, CavernHydrology.Sampler sampler,
CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
return new Search(seed, sampler, water, pocket).find();
}
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
private record Node(Position at, int reach) {}
private record Trace(List<Position> path, CavernHydrology.FlowBounds bounds) {}
private static final class Search {
private final long seed;
private final CavernHydrology.Sampler sampler;
private final CavernHydrology.Plan water;
private final CavernLavaDeposit.Plan pocket;
private final List<Fall> falls = new ArrayList<>();
Search(long seed, CavernHydrology.Sampler sampler, CavernHydrology.Plan water, CavernLavaDeposit.Plan pocket) {
this.seed = seed; this.sampler = sampler; this.water = water; this.pocket = pocket;
}
List<Fall> find() {
List<Candidate> candidates = new ArrayList<>();
// Coarse probes below the surface can see a lower overhang that a heightmap hides.
for (int x = -216; x <= 216; x += 8) for (int z = -216; z <= 216; z += 8) {
if ((long) x * x + (long) z * z >= (long) RADIUS * RADIUS) continue;
for (int y = MIN_SOURCE_Y; y <= MAX_SOURCE_Y; y += 8) {
if (!solid(x, y, z)) continue;
for (int[] d : CARDINALS) {
if (solid(x + d[0] * 4, y, z + d[1] * 4)) continue;
double score = random(seed + y, x + d[0], z + d[1])
+ (MAX_SOURCE_Y - y) * .002;
candidates.add(new Candidate(x, y, z, d[0], d[1], score));
}
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z)
.thenComparingInt(Candidate::y).thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (falls.size() >= MAX_FALLS || attempts++ >= 160) break;
boolean placed = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, 2, -2, 4, -4}) {
Position source = new Position(x, candidate.y() + dy, z);
if (source.y() < MIN_SOURCE_Y || source.y() > MAX_SOURCE_Y || !separated(source)) continue;
List<Position> supports = niche(source, candidate.dx(), candidate.dz());
if (supports == null) continue;
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
Trace flow = trace(source, outlet);
if (flow == null || flow.bounds().minY() > source.y() - 12 || !clearOfWater(flow.bounds())) continue;
long id = mix(seed ^ (0x1A7AFA11L + falls.size() * 0x9E3779B97F4A7C15L));
falls.add(new Fall(id, source, outlet, flow.path(), flow.bounds(), supports));
placed = true;
break;
}
if (placed) break;
}
if (placed) break;
}
}
return List.copyOf(falls);
}
private boolean separated(Position source) {
CavernHydrology.FlowBounds point = bounds(source);
if (!clearOfWater(point)) return false;
for (Fall fall : falls) {
if (distanceSquared(source, fall.source()) < 80L * 80
|| distanceSquared(point, fall.flowBounds()) < 40L * 40) return false;
}
for (var cell : pocket.cells()) {
if (distanceSquared(source, new Position(cell.x(), cell.y(), cell.z())) < 32L * 32) return false;
}
return true;
}
private List<Position> niche(Position source, int dx, int dz) {
if (!solid(source)) return null;
Set<Position> supports = new HashSet<>();
// The source sits in rock, with a short intact roof, floor and three back/side faces.
// The spill column needs no floor: its absence is precisely what makes a cascade.
for (int dy = -3; dy <= 8; dy++) {
if (dy == 0) continue;
Position at = new Position(source.x(), source.y() + dy, source.z());
if (!solid(at)) return null;
supports.add(at);
}
for (int[] d : CARDINALS) {
if (d[0] == dx && d[1] == dz) continue;
for (int depth = 1; depth <= 2; depth++) {
Position at = new Position(source.x() + d[0] * depth, source.y(), source.z() + d[1] * depth);
if (!solid(at)) return null;
supports.add(at);
}
}
for (int dy = 0; dy >= -8; dy--) {
if (solid(source.x() + dx, source.y() + dy, source.z() + dz)) return null;
}
return supports.stream().sorted(POSITION_ORDER).toList();
}
private boolean clearOfWater(CavernHydrology.FlowBounds bounds) {
for (var cell : water.cells()) {
var column = new CavernHydrology.FlowBounds(cell.x(), cell.bedY() - cell.sedimentDepth() - 1,
cell.z(), cell.x(), Math.max(cell.carveTop(), cell.waterY()), cell.z());
if (distanceSquared(bounds, column) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spring : water.springs()) {
if (distanceSquared(bounds, spring.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
for (var spill : water.spills()) {
if (distanceSquared(bounds, spill.flowBounds()) < (long) WATER_CLEARANCE * WATER_CLEARANCE) return false;
}
return true;
}
/** Descending lava resets its horizontal reach to three in this non-ultrawarm dimension.
* Explore all downhill branches through rock, so selection does not depend on chunk order. */
private Trace trace(Position source, Position outlet) {
Map<Position, Integer> reach = new HashMap<>();
Map<Position, Position> parent = new HashMap<>();
ArrayDeque<Node> queue = new ArrayDeque<>();
reach.put(source, HORIZONTAL_REACH);
queue.addLast(new Node(source, HORIZONTAL_REACH));
Position lowest = source;
while (!queue.isEmpty()) {
Node node = queue.removeFirst();
Position at = node.at();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 12000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (!flowSolid(below, source)) {
enqueue(below, HORIZONTAL_REACH, at, reach, parent, queue);
continue;
}
if (node.reach() <= 0) continue;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!flowSolid(next, source)) enqueue(next, node.reach() - 1, at, reach, parent, queue);
}
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
for (Position at : reach.keySet()) {
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x());
minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Trace(List.copyOf(path), new CavernHydrology.FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Position source) {
if (at.equals(source)) return false;
var cell = water.cellAt(at.x(), at.y(), at.z());
if (cell != null && at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
return solid(at);
}
private boolean solid(Position at) { return solid(at.x(), at.y(), at.z()); }
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
}
private static void enqueue(Position next, int available, Position from, Map<Position, Integer> reach,
Map<Position, Position> parent, ArrayDeque<Node> queue) {
if (available <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, available);
queue.addLast(new Node(next, available));
}
private static CavernHydrology.FlowBounds bounds(Position at) {
return new CavernHydrology.FlowBounds(at.x(), at.y(), at.z(), at.x(), at.y(), at.z());
}
private static long distanceSquared(Position a, Position b) {
long dx = a.x() - b.x(), dy = a.y() - b.y(), dz = a.z() - b.z();
return dx * dx + dy * dy + dz * dz;
}
private static long distanceSquared(CavernHydrology.FlowBounds a, CavernHydrology.FlowBounds b) {
long dx = Math.max(0, Math.max((long) a.minX() - b.maxX(), (long) b.minX() - a.maxX()));
long dy = Math.max(0, Math.max((long) a.minY() - b.maxY(), (long) b.minY() - a.maxY()));
long dz = Math.max(0, Math.max((long) a.minZ() - b.maxZ(), (long) b.minZ() - a.maxZ()));
return dx * dx + dy * dy + dz * dz;
}
private static double random(long seed, int x, int z) {
return (mix(seed ^ ((long) x << 32) ^ (z & 0xffffffffL)) >>> 11) * 0x1.0p-53;
}
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -0,0 +1,44 @@
package fr.koka.sanctuary.worldgen;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
/** Alpha.9 shore deposits: broad continuous patches through the existing sediment thickness. */
public final class CavernMaterials {
private CavernMaterials() {}
public static BlockState sedimentBlock(CavernHydrology.Cell cell, long seed, int depth) {
if (cell.material() == CavernHydrology.ShoreMaterial.GRASS) {
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).defaultBlockState();
}
if (cell.material() == CavernHydrology.ShoreMaterial.CLAY) return Blocks.CLAY.defaultBlockState();
double patch = patch(seed, cell.x(), cell.z(), 32);
Block material;
if (cell.hasWater() && patch < 0.24) material = Blocks.CLAY;
else if (patch < 0.53) material = Blocks.SAND;
else if (patch < 0.68) material = Blocks.GRAVEL;
else {
double rock = patch(seed ^ 0x4c6179657273L, cell.x(), cell.z(), 40);
material = rock < 0.28 ? Blocks.GRANITE : rock < 0.45 ? Blocks.DIORITE
: rock < 0.65 ? Blocks.ANDESITE : Blocks.STONE;
}
return material.defaultBlockState();
}
private static double patch(long seed, int x, int z, int scale) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale);
double tz = smooth(Math.floorMod(z, scale) / (double) scale);
double near = lerp(value(seed, gx, gz), value(seed, gx + 1, gz), tx);
double far = lerp(value(seed, gx, gz + 1), value(seed, gx + 1, gz + 1), tx);
return lerp(near, far, tz);
}
private static double value(long seed, int x, int z) {
return (WoodlandGroveGeometry.seed(seed, x, 0, z, 0x53484f5245L) >>> 11) * 0x1.0p-53;
}
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double lerp(double a, double b, double t) { return a + (b - a) * t; }
}
@@ -0,0 +1,41 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.feature.Feature;
/** A few plants on existing suitable shores, after the forest has been decorated. */
public final class CavernShoreSugarCaneFeature implements Feature {
public static final MapCodec<CavernShoreSugarCaneFeature> CODEC = MapCodec.unit(CavernShoreSugarCaneFeature::new);
@Override
public MapCodec<CavernShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !CavernHydrologyRuntime.enabled(noise)) return false;
var plan = CavernHydrologyRuntime.plan(noise, level.getLevel().getChunkSource().randomState());
var cane = Blocks.SUGAR_CANE.defaultBlockState();
boolean placed = false;
for (var cell : plan.cellsInChunk(origin.getX() >> 4, origin.getZ() >> 4)) {
if (cell.hasWater() || random.nextInt(7) != 0) continue;
BlockPos base = new BlockPos(cell.x(), cell.bedY() + 1, cell.z());
if (!level.getBlockState(base).isAir() || !cane.canSurvive(level, base)) continue;
int height = 1 + random.nextInt(3);
for (int y = 0; y < height; y++) {
BlockPos pos = base.above(y);
if (!level.getBlockState(pos).isAir() || !cane.canSurvive(level, pos)) break;
setBlock(level, pos, cane);
placed = true;
}
}
return placed;
}
}
@@ -0,0 +1,108 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.VegetationBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.material.Fluids;
/** One-time alpha.9 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class CavernSpringOutlets {
public record Pending(List<CavernHydrology.Position> sources, List<CavernHydrology.Position> outlets) {
public static final Pending EMPTY = new Pending(List.of(), List.of());
public Pending { sources = List.copyOf(sources); outlets = List.copyOf(outlets); }
public boolean isEmpty() { return sources.isEmpty() && outlets.isEmpty(); }
}
private CavernSpringOutlets() {}
/** Called before vanilla consumes the queue; a fully processed/reloaded chunk has no work. */
public static Pending capturePending(ServerLevel level, LevelChunk chunk) {
if (!(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !CavernHydrologyRuntime.enabled(noise)) return Pending.EMPTY;
boolean queued = false;
for (var section : chunk.getPostProcessing()) if (section != null && !section.isEmpty()) {
queued = true;
break;
}
if (!queued) return Pending.EMPTY;
var plan = CavernHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<CavernHydrology.Position> sources = new ArrayList<>(), outlets = new ArrayList<>();
for (var spring : plan.springs()) {
if (markedHere(chunk, spring.source())) sources.add(spring.source());
if (markedHere(chunk, spring.outlet())) outlets.add(spring.outlet());
}
for (var spill : plan.spills()) {
if (markedHere(chunk, spill.source())) sources.add(spill.source());
if (markedHere(chunk, spill.outlet())) outlets.add(spill.outlet());
}
for (var fall : CavernHydrologyRuntime.lavaPlan(noise, level.getChunkSource().randomState()).falls()) {
var source = new CavernHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
var outlet = new CavernHydrology.Position(fall.outlet().x(), fall.outlet().y(), fall.outlet().z());
if (markedHere(chunk, source)) sources.add(source);
if (markedHere(chunk, outlet)) outlets.add(outlet);
}
return new Pending(sources, outlets);
}
/** All neighbouring FEATURES have finished before a chunk reaches this stage. */
public static void finish(ServerLevel level, LevelChunk chunk, Pending pending) {
if (pending.isEmpty() || !(level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator noise)
|| !CavernHydrologyRuntime.enabled(noise)) return;
var random = level.getChunkSource().randomState();
var density = noise.generatorSettings().value().noiseRouter().finalDensity();
for (var outlet : pending.outlets()) {
if (!owns(chunk, outlet)) throw new IllegalArgumentException("An outlet belongs to another chunk: " + outlet);
var modified = CavernHydrologyRuntime.plan(noise, random).cellAt(outlet.x(), outlet.y(), outlet.z());
boolean carved = modified != null && outlet.y() > modified.waterY() && outlet.y() > modified.bedY()
&& outlet.y() <= modified.carveTop();
if (!carved && random.sampleBlockValueUncached(density, outlet.x(), outlet.y(), outlet.z()) > 0) {
throw new IllegalStateException("A spring outlet must already be natural air: " + outlet);
}
BlockPos pos = blockPos(outlet);
BlockState current = chunk.getBlockState(pos);
if (removableDecoration(current)) {
// Only this declared air cell is reopened. Neighbour notification wakes the
// adjacent source even if its own chunk was post-processed first.
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
for (var source : pending.sources()) {
if (!owns(chunk, source)) throw new IllegalArgumentException("A source belongs to another chunk: " + source);
BlockPos pos = blockPos(source);
BlockState current = chunk.getBlockState(pos);
if (current.getFluidState().isSource()) {
if (current.is(Blocks.WATER)) level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level));
else if (current.is(Blocks.LAVA)) level.scheduleTick(pos, Fluids.LAVA, Fluids.LAVA.getTickDelay(level));
}
}
}
private static boolean markedHere(LevelChunk chunk, CavernHydrology.Position position) {
if (!owns(chunk, position)) return false;
var queue = chunk.getPostProcessing()[chunk.getSectionIndex(position.y())];
return queue != null && queue.contains(ProtoChunk.packOffsetCoordinates(blockPos(position)));
}
private static boolean owns(LevelChunk chunk, CavernHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(CavernHydrology.Position position) {
return new BlockPos(position.x(), position.y(), position.z());
}
private static boolean removableDecoration(BlockState state) {
if (state.hasBlockEntity()) return false;
return state.is(Blocks.GLOW_LICHEN) || state.is(Blocks.VINE) || state.is(Blocks.MOSS_CARPET)
|| state.getBlock() instanceof VegetationBlock || state.is(BlockTags.LEAVES) || state.is(BlockTags.LOGS);
}
}
@@ -0,0 +1,435 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.CavernHydrology.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.function.Predicate;
/** Small connected water terraces inside existing cave ledges. Each retained pool is excavated
* in rock; one declared edge may overflow. No dam, shelf, cave fill or fluid column is generated. */
public final class CavernTerraces {
public static final int MIN_Y = 56, MAX_Y = 208;
public static final int MAX_GROUPS = 2, MAX_DEPTH = 3, MAX_CARVE = 5;
private static final int RADIUS = 216, SEDIMENT = 3;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private CavernTerraces() {}
public record Result(List<Cell> cells, List<Feature> features, List<Terrace> terraces,
List<Spill> spills, Set<Position> predictedFlow) {
public static final Result EMPTY = new Result(List.of(), List.of(), List.of(), List.of(), Set.of());
public Result {
cells = List.copyOf(cells); features = List.copyOf(features); terraces = List.copyOf(terraces);
spills = List.copyOf(spills); predictedFlow = Set.copyOf(predictedFlow);
}
}
public static Result create(long seed, Sampler sampler, CavernHydrology.Plan surface) {
return new Search(seed, sampler, surface).build();
}
private record Candidate(int x, int y, int z, int dx, int dz, double score) {}
private record Floor(int top, int bottom, int ceiling) {}
private record Node(int x, int z, double cost) {}
private record FlowNode(Position at, int reach) {}
private record Flow(Set<Position> volume, List<Position> path, FlowBounds bounds) {}
private record Pool(Map<Long, Cell> cells, Position source, Position outlet) {}
private static final Floor NO_FLOOR = new Floor(-1, -1, -1);
private static final class Search {
private final long seed;
private final Sampler sampler;
private final CavernHydrology.Plan surface;
private final Map<Position, Floor> floors = new HashMap<>();
private final List<Cell> acceptedCells = new ArrayList<>();
private final List<Feature> features = new ArrayList<>();
private final List<Terrace> terraces = new ArrayList<>();
private final List<Spill> spills = new ArrayList<>();
private final Set<Position> predicted = new HashSet<>();
Search(long seed, Sampler sampler, CavernHydrology.Plan surface) {
this.seed = seed; this.sampler = sampler; this.surface = surface;
}
Result build() {
List<Candidate> candidates = new ArrayList<>();
for (int x = -208; x <= 208; x += 8) for (int z = -208; z <= 208; z += 8) {
if (!inside(x, z)) continue;
for (int y = 64; y <= MAX_Y; y += 8) {
if (!solid(x, y, z) || solid(x, y + 8, z)) continue;
int level = y;
for (int exact = y + 7; exact > y; exact--) if (solid(x, exact, z)) { level = exact; break; }
if (level > MAX_Y || floor(x, z, level) == NO_FLOOR) continue;
for (int[] d : CARDINALS) if (!solid(x + d[0] * 4, level, z + d[1] * 4)) {
candidates.add(new Candidate(x, level, z, d[0], d[1], random(seed + level, x + d[0], z + d[1])));
}
}
}
candidates.sort(Comparator.comparingDouble(Candidate::score).reversed()
.thenComparingInt(Candidate::x).thenComparingInt(Candidate::z).thenComparingInt(Candidate::y)
.thenComparingInt(Candidate::dx).thenComparingInt(Candidate::dz));
int attempts = 0;
for (Candidate candidate : candidates) {
if (terraces.size() >= MAX_GROUPS || attempts++ >= 160) break;
boolean added = false;
for (int tangent : new int[]{0, -2, 2}) {
for (int along = 0; along < 4; along++) {
int x = candidate.x() + candidate.dx() * along - candidate.dz() * tangent;
int z = candidate.z() + candidate.dz() * along + candidate.dx() * tangent;
for (int dy : new int[]{0, -1, 1, -2, 2}) {
Position source = new Position(x, candidate.y() + dy, z);
Position outlet = new Position(x + candidate.dx(), source.y(), z + candidate.dz());
if (!mouth(source, outlet) || !separated(source)) continue;
if (addGroup(source, outlet)) { added = true; break; }
}
if (added) break;
}
if (added) break;
}
}
return new Result(acceptedCells, features, terraces, spills, predicted);
}
private boolean addGroup(Position source, Position outlet) {
long groupId = mix(seed ^ (0xA9CA7E2L + terraces.size() * 0x9E3779B97F4A7C15L));
long upperId = mix(groupId + 1), lowerId = mix(groupId + 2);
Pool upper = grow(source, source.y(), source, outlet, upperId);
if (upper == null) return false;
// The first impact on a slope is often a thin lip. Follow natural drainage across
// its ledges, then find a supported receiving pool at a genuinely lower level.
// The same bounded fluid trace is used before and after carving the receiving pool.
Flow drainage = trace(source, outlet, new ArrayList<>(upper.cells().values()));
if (drainage == null) return false;
List<Position> contacts = new ArrayList<>();
for (Position at : drainage.volume()) {
int level = at.y() - 1;
if (level < MIN_Y || level > source.y() - 4 || !solid(at.x(), level, at.z())) continue;
Floor ground = floor(at.x(), at.z(), level);
if (ground != NO_FLOOR && ground.bottom() <= level - 7)
contacts.add(new Position(at.x(), level, at.z()));
}
contacts.sort(Comparator.comparingInt(Position::y).reversed()
.thenComparingInt(Position::x).thenComparingInt(Position::z));
Pool lower = null;
Position landing = null;
int attempts = 0;
for (Position contact : contacts) {
if (attempts++ >= 48) break;
lower = grow(contact, contact.y(), null, null, lowerId);
if (lower != null) { landing = contact; break; }
}
if (lower == null) return false;
List<Cell> groupCells = new ArrayList<>(upper.cells().values());
groupCells.addAll(lower.cells().values());
if (overlapping(groupCells)) return false;
Flow inlet = trace(source, outlet, groupCells);
if (inlet == null || !reaches(inlet, lower)) return false;
// If the same lower ledge has another natural edge, connect it to an open terminal
// fall. Otherwise its second pool stays closed; the first inter-pool cascade remains.
Pool drained = terminalPool(landing, lowerId);
Flow terminal = null;
if (drained != null) {
List<Cell> replacement = new ArrayList<>(upper.cells().values());
replacement.addAll(drained.cells().values());
if (!overlapping(replacement)) {
Flow in = trace(source, outlet, replacement);
Flow out = trace(drained.source(), drained.outlet(), replacement);
if (in != null && reaches(in, drained) && out != null
&& out.bounds().minY() <= drained.source().y() - 12) {
lower = drained; inlet = in; terminal = out; groupCells = replacement;
}
}
}
acceptedCells.addAll(groupCells);
features.add(feature(upperId, upper, source));
features.add(feature(lowerId, lower, landing));
terraces.add(new Terrace(groupId, List.of(upperId, lowerId)));
spills.add(new Spill(mix(groupId + 3), upperId, lowerId, source, outlet, inlet.path(), inlet.bounds()));
predicted.addAll(inlet.volume());
if (terminal != null) {
spills.add(new Spill(mix(groupId + 4), lowerId, -1, lower.source(), lower.outlet(), terminal.path(), terminal.bounds()));
predicted.addAll(terminal.volume());
}
return true;
}
private Pool terminalPool(Position center, long id) {
List<Position> candidates = new ArrayList<>();
for (int dx = -12; dx <= 12; dx++) for (int dz = -12; dz <= 12; dz++) {
if (dx * dx + dz * dz > 144) continue;
candidates.add(new Position(center.x() + dx, center.y(), center.z() + dz));
}
candidates.sort(Comparator.comparingDouble((Position p) -> Math.hypot(p.x() - center.x(), p.z() - center.z()))
.thenComparingInt(Position::x).thenComparingInt(Position::z));
int tried = 0;
for (Position source : candidates) for (int[] d : CARDINALS) {
Position outlet = new Position(source.x() + d[0], source.y(), source.z() + d[1]);
if (!mouth(source, outlet)) continue;
if (tried++ >= 12) return null;
Pool pool = grow(center, center.y(), source, outlet, id);
if (pool != null) return pool;
}
return null;
}
private Pool grow(Position center, int level, Position source, Position outlet, long id) {
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingDouble(Node::cost)
.thenComparingInt(Node::x).thenComparingInt(Node::z));
Map<Long, Double> costs = new HashMap<>();
Set<Long> wet = new HashSet<>();
long origin = key(center.x(), center.z());
queue.add(new Node(center.x(), center.z(), 0)); costs.put(origin, 0.0);
int target = 100 + (int) (random(seed + level, center.x(), center.z()) * 80);
while (!queue.isEmpty() && wet.size() < target) {
Node node = queue.remove(); long k = key(node.x(), node.z());
if (wet.contains(k) || node.cost() > costs.getOrDefault(k, Double.POSITIVE_INFINITY)
|| !poolColumn(node.x(), node.z(), level, source, outlet)) continue;
wet.add(k);
for (int[] d : NEIGHBORS) {
int x = node.x() + d[0], z = node.z() + d[1]; long next = key(x, z);
if (Math.hypot(x - center.x(), z - center.z()) > 16 || wet.contains(next)) continue;
double cost = node.cost() + Math.hypot(d[0], d[1]) * (1 + noise(x, z, 11) * 1.8);
if (cost >= costs.getOrDefault(next, Double.POSITIVE_INFINITY)) continue;
costs.put(next, cost); queue.add(new Node(x, z, cost));
}
}
wet = connected(wet, origin);
if (wet.size() < 40 || source != null && !wet.contains(key(source.x(), source.z()))) return null;
Map<Long, Cell> cells = new HashMap<>();
for (long k : wet) {
int x = (int) (k >> 32), z = (int) k;
boolean edge = false;
for (int[] d : CARDINALS) edge |= !wet.contains(key(x + d[0], z + d[1]));
int depth = edge ? 1 : 2 + (noise(x, z, 9) > .6 ? 1 : 0);
Floor floor = floor(x, z, level);
int bed = level - depth;
if (floor == NO_FLOOR || floor.top() - bed > MAX_CARVE || floor.bottom() > bed - SEDIMENT - 1) return null;
ShoreMaterial material = noise(x + 101, z - 87, 19) > .42 ? ShoreMaterial.CLAY : ShoreMaterial.STONE;
cells.put(k, new Cell(x, z, level, bed, floor.top(), material, id, SEDIMENT));
}
for (Cell cell : cells.values()) for (int[] d : CARDINALS) {
int x = cell.x() + d[0], z = cell.z() + d[1];
Cell next = cells.get(key(x, z));
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
if (next != null && y > next.bedY()) continue;
if (source != null && cell.x() == source.x() && cell.z() == source.z()
&& x == outlet.x() && z == outlet.z() && y == outlet.y()) continue;
if (!solid(x, y, z)) return null;
}
}
return new Pool(Map.copyOf(cells), source, outlet);
}
private boolean poolColumn(int x, int z, int level, Position source, Position outlet) {
if (!inside(x, z)) return false;
Floor floor = floor(x, z, level);
if (floor == NO_FLOOR || floor.bottom() > level - 7 || !available(x, z, level - 8, floor.top())) return false;
for (int[] d : CARDINALS) {
if (source != null && x == source.x() && z == source.z()
&& x + d[0] == outlet.x() && z + d[1] == outlet.z()) continue;
if (!solid(x + d[0], level, z + d[1])) return false;
}
return true;
}
private Floor floor(int x, int z, int level) {
return floors.computeIfAbsent(new Position(x, level, z), ignored -> {
if (level < MIN_Y || level > MAX_Y) return NO_FLOOR;
int top = -1;
for (int y = level + 2; y >= level; y--) {
if (solid(x, y, z)) { top = y; break; }
}
if (top < 0) return NO_FLOOR;
for (int y = top + 1; y <= top + 4; y++) if (solid(x, y, z)) return NO_FLOOR;
int ceiling = top + 5;
while (ceiling < 384 && !solid(x, ceiling, z)) ceiling++;
if (ceiling >= 384) return NO_FLOOR;
int bottom = top;
while (bottom > top - 16 && solid(x, bottom - 1, z)) bottom--;
return new Floor(top, bottom, ceiling);
});
}
private boolean mouth(Position source, Position outlet) {
if (source.y() < MIN_Y || source.y() > MAX_Y || !solid(source.x(), source.y(), source.z())) return false;
// An adjacent natural step may receive the water before it descends again. Requiring
// a vertical shaft here would discard the gentle ledges that form cave terraces.
if (solid(outlet.x(), outlet.y(), outlet.z())) return false;
return floor(source.x(), source.z(), source.y()) != NO_FLOOR;
}
private boolean available(int x, int z, int low, int high) {
for (Cell cell : surface.cellsAt(x, z)) if (low <= cell.carveTop() + 4
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
for (Cell cell : acceptedCells) if (cell.x() == x && cell.z() == z && low <= cell.carveTop() + 4
&& high >= cell.bedY() - cell.sedimentDepth() - 5) return false;
for (Spring spring : surface.springs()) if (inBounds(x, low, high, z, spring.flowBounds(), 6)) return false;
for (Spill spill : spills) if (inBounds(x, low, high, z, spill.flowBounds(), 6)) return false;
return true;
}
private boolean separated(Position source) {
for (Spill spill : spills) if (Math.hypot(source.x() - spill.source().x(), source.z() - spill.source().z()) < 64) return false;
return available(source.x(), source.z(), source.y() - 8, source.y() + 4);
}
private static boolean overlapping(List<Cell> cells) {
Map<Long, List<Cell>> columns = new HashMap<>();
for (Cell cell : cells) {
var existing = columns.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>());
for (Cell old : existing) if (cell.bedY() - cell.sedimentDepth() - 1 <= old.carveTop()
&& old.bedY() - old.sedimentDepth() - 1 <= cell.carveTop()) return true;
existing.add(cell);
}
return false;
}
private Flow trace(Position source, Position outlet, List<Cell> group) {
Map<Long, List<Cell>> changes = new HashMap<>();
Set<Position> retained = new HashSet<>();
for (Cell cell : group) {
changes.computeIfAbsent(key(cell.x(), cell.z()), ignored -> new ArrayList<>()).add(cell);
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) retained.add(new Position(cell.x(), y, cell.z()));
}
Map<Position, Boolean> solidCache = new HashMap<>();
Predicate<Position> open = at -> !solidCache.computeIfAbsent(at, p -> flowSolid(p, changes));
Predicate<Position> canEnter = at -> !retained.contains(at) && open.test(at);
Predicate<Position> hole = at -> open.test(new Position(at.x(), at.y() - 1, at.z()));
Map<Position, Integer> reach = new HashMap<>(); Map<Position, Position> parent = new HashMap<>();
ArrayDeque<FlowNode> queue = new ArrayDeque<>();
reach.put(source, 7); queue.addLast(new FlowNode(source, 7)); Position lowest = source;
while (!queue.isEmpty()) {
FlowNode node = queue.removeFirst(); Position at = node.at();
if (node.reach() < reach.getOrDefault(at, -1)) continue;
if (at.y() < lowest.y()) lowest = at;
if (reach.size() > 16000 || Math.abs(at.x() - source.x()) > 64 || Math.abs(at.z() - source.z()) > 64) return null;
// Vanilla never replaces an existing source. Record the arriving contact,
// but do not manufacture a route through the separately retained basin.
if (retained.contains(at) && !at.equals(source)) continue;
if (at.y() <= 0) continue;
Position below = new Position(at.x(), at.y() - 1, at.z());
if (open.test(below)) { enqueue(below, 7, at, reach, parent, queue); continue; }
if (node.reach() <= 0) continue;
for (Position next : downhillChoices(at, canEnter, hole))
enqueue(next, node.reach() - 1, at, reach, parent, queue);
}
if (!reach.containsKey(outlet)) return null;
int minX = source.x(), maxX = minX, minY = source.y(), maxY = minY, minZ = source.z(), maxZ = minZ;
for (Position at : reach.keySet()) {
minX = Math.min(minX, at.x()); maxX = Math.max(maxX, at.x()); minY = Math.min(minY, at.y()); maxY = Math.max(maxY, at.y());
minZ = Math.min(minZ, at.z()); maxZ = Math.max(maxZ, at.z());
}
List<Position> path = new ArrayList<>();
for (Position at = lowest; at != null; at = parent.get(at)) path.add(at);
Collections.reverse(path);
return new Flow(Set.copyOf(reach.keySet()), List.copyOf(path), new FlowBounds(minX, minY, minZ, maxX, maxY, maxZ));
}
private boolean flowSolid(Position at, Map<Long, List<Cell>> changes) {
for (Cell cell : changes.getOrDefault(key(at.x(), at.z()), List.of()))
if (at.y() > cell.bedY() && at.y() <= cell.carveTop()) return false;
Cell surfaceCell = surface.cellAt(at.x(), at.y(), at.z());
if (surfaceCell != null && at.y() > surfaceCell.bedY() && at.y() <= surfaceCell.carveTop()) return false;
return solid(at.x(), at.y(), at.z());
}
private static boolean reaches(Flow flow, Pool pool) {
return flow.volume().stream().anyMatch(at -> {
Cell cell = pool.cells().get(key(at.x(), at.z()));
return cell != null && at.y() > cell.bedY() && at.y() <= cell.waterY();
});
}
private static Feature feature(long id, Pool pool, Position center) {
int minX = Integer.MAX_VALUE, minZ = minX, maxX = Integer.MIN_VALUE, maxZ = maxX;
for (Cell cell : pool.cells().values()) {
minX = Math.min(minX, cell.x()); maxX = Math.max(maxX, cell.x()); minZ = Math.min(minZ, cell.z()); maxZ = Math.max(maxZ, cell.z());
}
return new Feature(id, Kind.TERRACE, center.y(), List.of(new Point(center.x(), center.z())), pool.cells().size(),
new Bounds(minX, minZ, maxX, maxZ));
}
private boolean solid(int x, int y, int z) { return sampler.sample(x, y, z) > 0; }
private double noise(int x, int z, int scale) {
int gx = Math.floorDiv(x, scale), gz = Math.floorDiv(z, scale);
double tx = smooth(Math.floorMod(x, scale) / (double) scale), tz = smooth(Math.floorMod(z, scale) / (double) scale);
double a = random(seed, gx, gz) * (1 - tx) + random(seed, gx + 1, gz) * tx;
double b = random(seed, gx, gz + 1) * (1 - tx) + random(seed, gx + 1, gz + 1) * tx;
return a * (1 - tz) + b * tz;
}
}
/** WaterFluid's bare-rock downhill choice: consider holes through four recursive
* steps, keep only the nearest directions (including ties), spread everywhere
* only when no direction finds a hole. Retained sources cannot be entered. */
static List<Position> downhillChoices(Position at, Predicate<Position> canEnter, Predicate<Position> hole) {
List<Position> selected = new ArrayList<>();
int best = 1000;
for (int[] d : CARDINALS) {
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!canEnter.test(next)) continue;
int cost = hole.test(next) ? 0 : slopeDistance(next, 1, -d[0], -d[1], canEnter, hole);
if (cost < best) selected.clear();
if (cost <= best) { selected.add(next); best = cost; }
}
return selected;
}
private static int slopeDistance(Position at, int distance, int backX, int backZ,
Predicate<Position> canEnter, Predicate<Position> hole) {
int best = 1000;
for (int[] d : CARDINALS) {
if (d[0] == backX && d[1] == backZ) continue;
Position next = new Position(at.x() + d[0], at.y(), at.z() + d[1]);
if (!canEnter.test(next)) continue;
if (hole.test(next)) return distance;
if (distance < 4) best = Math.min(best,
slopeDistance(next, distance + 1, -d[0], -d[1], canEnter, hole));
}
return best;
}
private static void enqueue(Position next, int amount, Position from, Map<Position, Integer> reach,
Map<Position, Position> parent, ArrayDeque<FlowNode> queue) {
if (amount <= reach.getOrDefault(next, -1)) return;
if (!reach.containsKey(next)) parent.put(next, from);
reach.put(next, amount); queue.addLast(new FlowNode(next, amount));
}
private static Set<Long> connected(Set<Long> footprint, long origin) {
if (!footprint.contains(origin)) return Set.of();
Set<Long> seen = new HashSet<>(); ArrayDeque<Long> queue = new ArrayDeque<>();
seen.add(origin); queue.add(origin);
while (!queue.isEmpty()) {
long at = queue.removeFirst(); int x = (int) (at >> 32), z = (int) at;
for (int[] d : CARDINALS) {
long next = key(x + d[0], z + d[1]);
if (footprint.contains(next) && seen.add(next)) queue.addLast(next);
}
}
return seen;
}
private static boolean inBounds(int x, int low, int high, int z, FlowBounds b, int margin) {
return x >= b.minX() - margin && x <= b.maxX() + margin && z >= b.minZ() - margin && z <= b.maxZ() + margin
&& low <= b.maxY() + margin && high >= b.minY() - margin;
}
private static boolean inside(int x, int z) { return (long) x * x + (long) z * z < (long) RADIUS * RADIUS; }
private static long key(int x, int z) { return ((long) x << 32) | (z & 0xffffffffL); }
private static double smooth(double t) { return t * t * (3 - 2 * t); }
private static double random(long seed, int x, int z) { return (mix(seed ^ key(x, z)) >>> 11) * 0x1.0p-53; }
private static long mix(long value) {
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
return value ^ (value >>> 31);
}
}
@@ -33,6 +33,9 @@ public final class SanctuarySpawn {
public static final ResourceKey<NoiseGeneratorSettings> RIFT_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_rift"));
public static final ResourceKey<NoiseGeneratorSettings> CAVERN_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_cavern"));
private SanctuarySpawn() {}
public static boolean usesSanctuaryGenerator(ServerLevel level) {
@@ -43,7 +46,8 @@ public final class SanctuarySpawn {
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|| generator.generatorSettings().is(RIFT_SETTINGS));
|| generator.generatorSettings().is(RIFT_SETTINGS)
|| generator.generatorSettings().is(CAVERN_SETTINGS));
}
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
@@ -27,5 +27,15 @@
"biome.sanctuary.rift_rocky_heath": "Rocky Heath",
"biome.sanctuary.rift_dark_grove": "Dark Undergrowth",
"biome.sanctuary.rift_bamboo_grove": "Bamboo Grove",
"biome.sanctuary.rift_sulfur_depths": "Sulfur Depths"
"biome.sanctuary.rift_sulfur_depths": "Sulfur Depths",
"biome.sanctuary.cavern_oak_forest": "Sanctuary Oak Woodland",
"biome.sanctuary.cavern_birch_forest": "Sanctuary Birch Woodland",
"biome.sanctuary.cavern_clearing": "Flowered Clearing",
"biome.sanctuary.cavern_dry_woodland": "Dry Woodland",
"biome.sanctuary.cavern_rocky_heath": "Rocky Heath",
"biome.sanctuary.cavern_dark_grove": "Dark Undergrowth",
"biome.sanctuary.cavern_bamboo_grove": "Bamboo Grove",
"biome.sanctuary.cavern_sulfur_depths": "Sulfur Depths",
"biome.sanctuary.cavern_lush_caves": "Sanctuary Lush Caves",
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves"
}
@@ -27,5 +27,15 @@
"biome.sanctuary.rift_rocky_heath": "Lande rocheuse",
"biome.sanctuary.rift_dark_grove": "Sous-bois sombre",
"biome.sanctuary.rift_bamboo_grove": "Bosquet de bambous",
"biome.sanctuary.rift_sulfur_depths": "Profondeurs soufrées"
"biome.sanctuary.rift_sulfur_depths": "Profondeurs soufrées",
"biome.sanctuary.cavern_oak_forest": "Chênaie de Sanctuary",
"biome.sanctuary.cavern_birch_forest": "Bétulaie de Sanctuary",
"biome.sanctuary.cavern_clearing": "Clairière fleurie",
"biome.sanctuary.cavern_dry_woodland": "Bois sec",
"biome.sanctuary.cavern_rocky_heath": "Lande rocheuse",
"biome.sanctuary.cavern_dark_grove": "Sous-bois sombre",
"biome.sanctuary.cavern_bamboo_grove": "Bosquet de bambous",
"biome.sanctuary.cavern_sulfur_depths": "Profondeurs soufrées",
"biome.sanctuary.cavern_lush_caves": "Grottes luxuriantes de Sanctuary",
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary"
}
@@ -0,0 +1,167 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.sparse_jungle"
}
},
"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:chicken",
"count": 4,
"weight": 10
},
{
"type": "minecraft:wolf",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 8
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.95,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,162 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.7,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:forest_flowers",
"minecraft:wildflowers_birch_forest",
"minecraft:trees_birch",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,164 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.75,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:patch_tall_grass_2",
"minecraft:patch_grass_meadow",
"minecraft:flower_meadow",
"minecraft:trees_meadow",
"minecraft:wildflowers_meadow",
"minecraft:flower_flower_forest",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.55,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.5
}
@@ -0,0 +1,163 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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:wolf",
"count": 4,
"weight": 5
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.4,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:forest_flowers",
"sanctuary:rift_sparse_trees",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.75
}
@@ -0,0 +1,153 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.9,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.5
}
@@ -0,0 +1,166 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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:wolf",
"count": 4,
"weight": 5
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.8,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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",
"minecraft:forest_flowers",
"minecraft:trees_birch_and_oak_leaf_litter",
"minecraft:patch_bush",
"minecraft:flower_default",
"minecraft:patch_grass_forest",
"minecraft:brown_mushroom_normal",
"minecraft:red_mushroom_normal",
"minecraft:patch_pumpkin",
"minecraft:patch_sugar_cane",
"minecraft:patch_firefly_bush_near_water",
"sanctuary:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,158 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.meadow"
}
},
"minecraft:gameplay/natural_mob_spawns": {
"argument": {
"spawn_costs": {},
"spawns_by_category": {
"ambient": [
{
"type": "minecraft:bat",
"count": 8,
"weight": 10
}
],
"creature": [
{
"type": "minecraft:donkey",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 2,
"min_inclusive": 1
},
"weight": 1
},
{
"type": "minecraft:rabbit",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 6,
"min_inclusive": 2
},
"weight": 2
},
{
"type": "minecraft:sheep",
"count": {
"type": "minecraft:uniform",
"max_inclusive": 4,
"min_inclusive": 2
},
"weight": 2
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.65,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,156 @@
{
"attributes": {
"minecraft:audio/background_music": {
"default": {
"max_delay": 24000,
"min_delay": 12000,
"sound": "minecraft:music.overworld.forest"
}
},
"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:wolf",
"count": 4,
"weight": 5
}
],
"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": 100
},
{
"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
}
],
"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": "#79a6ff"
},
"carvers": [],
"downfall": 0.5,
"effects": {
"water_color": "#3f76e4",
"grass_color": "#88b96b",
"foliage_color": "#79aa61",
"dry_foliage_color": "#b9a766"
},
"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:cavern_groves",
"sanctuary:cavern_decorations",
"sanctuary:cavern_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,39 @@
{
"type": "minecraft:geode",
"blocks": {
"alternate_inner_layer_provider": {
"id": "minecraft:budding_amethyst"
},
"cannot_replace": "#minecraft:features_cannot_replace",
"filling_provider": {
"id": "minecraft:air"
},
"inner_layer_provider": {
"id": "minecraft:amethyst_block"
},
"inner_placements": [
"minecraft:small_amethyst_bud",
"minecraft:medium_amethyst_bud",
"minecraft:large_amethyst_bud",
"minecraft:amethyst_cluster"
],
"invalid_blocks": "#minecraft:geode_invalid_blocks",
"middle_layer_provider": {
"id": "minecraft:calcite"
},
"outer_layer_provider": {
"id": "minecraft:smooth_basalt"
}
},
"crack": {
"generate_crack_chance": 1.0
},
"invalid_blocks_threshold": 1,
"layers": {},
"outer_wall_distance": 1,
"use_alternate_layer0_chance": 0.2,
"distribution_points": 2,
"point_offset": 1,
"min_gen_offset": -5,
"max_gen_offset": 7
}
@@ -0,0 +1,37 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -1000,
"max_threshold": -0.3,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:blackstone"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": 0.3,
"max_threshold": 1000,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:basalt"
}
},
{
"type": "minecraft:block",
"result_state": "minecraft:deepslate"
}
]
}
@@ -0,0 +1,71 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 136
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:cavern_deep_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 152
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 168
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_deep_strata"
}
},
"sanctuary:cavern_upper_strata"
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -1000,
"max_threshold": -0.36,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:cobblestone"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": 0.36,
"max_threshold": 1000,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:packed_mud"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -0.14,
"max_threshold": 0.14,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:mossy_cobblestone"
}
},
{
"type": "minecraft:block",
"result_state": "minecraft:tuff"
}
]
}
@@ -0,0 +1,279 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_sulfur_depths"
},
"then_run": "sanctuary:cavern_sulfur_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 136
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:cavern_deep_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 152
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 168
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_lush_caves"
},
"then_run": "sanctuary:cavern_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_dark_grove"
},
"then_run": {
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 192
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:cavern_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 208
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_humid_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 224
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_bamboo_grove"
},
"then_run": {
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 192
},
"surface_depth_multiplier": 0
}
},
"then_run": "sanctuary:cavern_humid_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 208
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_humid_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:not",
"invert": {
"type": "minecraft:y_above",
"add_stone_depth": false,
"anchor": {
"absolute": 224
},
"surface_depth_multiplier": 0
}
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:cavern_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_rocky_heath"
},
"then_run": "sanctuary:cavern_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_exposure",
"min_threshold": 0.85,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:cavern_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:cavern_dry_woodland"
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_exposure",
"min_threshold": 0.15,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:cavern_upper_strata"
}
},
"sanctuary:starter_island",
"sanctuary:cavern_upper_strata"
]
}
@@ -0,0 +1,34 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -0.15,
"max_threshold": 0.55,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:sulfur"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -0.55,
"max_threshold": -0.3,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:cinnabar"
}
},
"sanctuary:cavern_depth_strata"
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -1000,
"max_threshold": -0.36,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:granite"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": 0.36,
"max_threshold": 1000,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:diorite"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:cavern_strata",
"min_threshold": -0.14,
"max_threshold": 0.14,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:andesite"
}
},
{
"type": "minecraft:block",
"result_state": "minecraft:stone"
}
]
}
@@ -0,0 +1,5 @@
{
"base_amplitude": 1.0,
"base_octave": -6,
"octave_count": 2
}
@@ -0,0 +1,5 @@
{
"base_amplitude": 1.0,
"base_octave": -7,
"octave_count": 2
}
@@ -0,0 +1,5 @@
{
"base_amplitude": 1.0,
"base_octave": -6,
"octave_count": 2
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:cavern_island",
"noise": {
"height": 384,
"min_y": 0
},
"noise_router": {
"chunk_surface_level": 0.0,
"continents": 0.0,
"depth": 0.0,
"erosion": 0.0,
"final_density": "sanctuary:final_density_rift",
"ridges": 0.0,
"temperature": "sanctuary:temperate_variation",
"vegetation": "sanctuary:temperate_moisture"
},
"sea_level": -64,
"spawn_target": []
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:cavern_decorations"
},
"placement": []
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:cavern_groves"
},
"placement": []
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:cavern_shore_sugar_cane"
},
"placement": []
}
@@ -5,17 +5,19 @@
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "sanctuary:rift_island",
"oak_forest": "sanctuary:rift_oak_forest",
"birch_forest": "sanctuary:rift_birch_forest",
"clearing": "sanctuary:rift_clearing",
"dry_woodland": "sanctuary:rift_dry_woodland",
"dark_grove": "sanctuary:rift_dark_grove",
"bamboo_grove": "sanctuary:rift_bamboo_grove",
"sulfur_depths": "sanctuary:rift_sulfur_depths",
"rocky_heath": "sanctuary:rift_rocky_heath"
"type": "sanctuary:cavern_island",
"oak_forest": "sanctuary:cavern_oak_forest",
"birch_forest": "sanctuary:cavern_birch_forest",
"clearing": "sanctuary:cavern_clearing",
"dry_woodland": "sanctuary:cavern_dry_woodland",
"dark_grove": "sanctuary:cavern_dark_grove",
"bamboo_grove": "sanctuary:cavern_bamboo_grove",
"sulfur_depths": "sanctuary:cavern_sulfur_depths",
"rocky_heath": "sanctuary:cavern_rocky_heath",
"lush_caves": "sanctuary:cavern_lush_caves",
"dripstone_caves": "sanctuary:cavern_dripstone_caves"
},
"settings": "sanctuary:sanctuary_rift"
"settings": "sanctuary:sanctuary_cavern"
}
},
"minecraft:the_end": {
@@ -7,7 +7,8 @@
"SurfaceHydrologyMixin",
"LayeredSpringOutletMixin",
"WoodlandSpringOutletMixin",
"RiftSpringOutletMixin"
"RiftSpringOutletMixin",
"CavernSpringOutletMixin"
],
"client": [
"client.SanctuarySkyMixin",
@@ -0,0 +1,140 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.CavernHydrology.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/** Geometry tests complement engine checks of generated fluids, plants and post-processing. */
public final class CavernHydrologySmoke {
private CavernHydrologySmoke() {}
public static void main(String[] args) {
verifyDownhillChoice();
var empty = CavernHydrology.create(0, (x, y, z) -> -1);
check(empty.cells().isEmpty() && empty.terraces().isEmpty() && empty.spills().isEmpty(), "void must stay empty");
Sampler ledges = (x, y, z) -> {
if (Math.hypot(x, z) >= 160) return -1;
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
return y >= 40 && y <= floor || y >= 200 && y <= 220 ? 1 : -1;
};
var first = CavernTerraces.create(0, ledges, empty);
check(!first.terraces().isEmpty(), "natural reference ledges must create connected terraces");
check(first.equals(CavernTerraces.create(0, ledges, empty)), "same seed must replay immutable terrace geometry");
check(!first.equals(CavernTerraces.create(42, ledges, empty)), "seed must affect terrace positions");
verify(first, ledges);
Sampler steppedSlope = (x, y, z) -> {
if (Math.hypot(x, z) >= 160) return -1;
int floor = 144 - Math.min(4, Math.max(0, Math.floorDiv(x + 2, 3)));
return y >= 40 && y <= floor || y >= 200 && y <= 220 ? 1 : -1;
};
var stepped = CavernTerraces.create(0, steppedSlope, empty);
check(!stepped.terraces().isEmpty(), "gentle natural steps must lead to a lower receiving pool");
verify(stepped, steppedSlope);
check(stepped.spills().stream().filter(s -> s.toFeatureId() != -1).anyMatch(s ->
steppedSlope.sample(s.outlet().x(), s.outlet().y() - 1, s.outlet().z()) > 0),
"reference drainage must cross a shallow first impact, not require an immediate shaft");
var noRoof = CavernTerraces.create(0, (x, y, z) -> y >= 200 ? -1 : ledges.sample(x, y, z), empty);
check(noRoof.terraces().isEmpty(), "lower terraces must have a natural cave roof");
var thin = CavernTerraces.create(0, (x, y, z) -> {
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
return y < floor && y > floor - 8 ? -1 : ledges.sample(x, y, z);
}, empty);
check(thin.terraces().isEmpty(), "perforated foundations cannot receive terraced pools");
// This complete plan contains surface water on the roof above the cavity. Lookups must
// retain both levels in the same X/Z column and must never carve the natural roof away.
Sampler stackedLedge = (x, y, z) -> {
if (Math.hypot(x, z) >= 100 || y < 40) return -1;
int roofTop = Math.abs(x) < 28 && Math.abs(z) < 28 ? 216 : 220;
int floor = x <= 0 ? 144 : x <= 24 ? 120 : 96;
boolean cavern = Math.abs(z) <= 12 && x >= -48 && x <= 48;
return y <= roofTop && (!cavern || y <= floor || y >= 196) ? 1 : -1;
};
var stacked = CavernHydrology.create(0, stackedLedge);
check(!stacked.terraces().isEmpty(), "surface water must not suppress safe lower layers");
boolean shared = false;
for (var cell : stacked.cells()) {
check(stacked.cellAt(cell.x(), cell.bedY(), cell.z()).equals(cell), "height-aware lookup loses a layer");
check(stacked.cellsInChunk(cell.x() >> 4, cell.z() >> 4).contains(cell), "chunk slice loses a layer");
var layers = stacked.cellsAt(cell.x(), cell.z());
shared |= layers.size() > 1;
for (var other : layers) if (!other.equals(cell)) check(other.carveTop() < cell.bedY() - cell.sedimentDepth() - 1
|| cell.carveTop() < other.bedY() - other.sedimentDepth() - 1, "vertical edits overlap");
}
check(shared, "reference world must exercise two modified layers in one X/Z column");
for (var spill : stacked.spills()) {
check(stacked.isSpillOpening(spill.outlet().x(), spill.outlet().y(), spill.outlet().z()), "declared outlet missing");
check(stacked.allowsSpringFlow(spill.outlet().x(), spill.outlet().y(), spill.outlet().z()), "flow prediction omits spill");
}
int maxLakeDepth = stacked.cells().stream().filter(c -> stacked.features().stream()
.anyMatch(f -> f.id() == c.featureId() && f.kind() == Kind.LAKE))
.mapToInt(c -> c.waterY() - c.bedY()).max().orElse(0);
check(maxLakeDepth >= 6 && maxLakeDepth <= CavernHydrology.MAX_DEPTH, "surface lake must deepen within natural support budget");
System.out.println("CavernHydrologySmoke: linked cave terraces, gravity paths, natural shells, stacked layers and deeper surface lakes passed.");
}
private static void verifyDownhillChoice() {
Position start = new Position(0, 100, 0);
// Two open branches have identical floors at the junction, but water must
// choose the east drop two steps away over the west drop four steps away.
var nearer = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5,
p -> p.x() == 2 || p.x() == -4);
check(nearer.equals(java.util.List.of(new Position(1, 100, 0))),
"a longer competing drop must not invent a water route into its receiving basin");
var tied = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5,
p -> Math.abs(p.x()) == 2);
check(new HashSet<>(tied).equals(java.util.Set.of(new Position(1, 100, 0), new Position(-1, 100, 0))),
"equally near drops must retain both vanilla branches");
var closed = CavernTerraces.downhillChoices(start, p -> p.z() == 0 && Math.abs(p.x()) <= 5, p -> false);
check(new HashSet<>(closed).equals(new HashSet<>(tied)), "flat floor without a nearby drop spreads to both sides");
}
private static void verify(CavernTerraces.Result result, Sampler density) {
Map<Long, Feature> features = new HashMap<>();
result.features().forEach(feature -> features.put(feature.id(), feature));
for (var group : result.terraces()) {
check(group.basinIds().size() == 2, "two retained steps per group");
var upper = features.get(group.basinIds().get(0)); var lower = features.get(group.basinIds().get(1));
check(upper.waterY() - lower.waterY() >= 4, "terraces require distinct gravity levels");
var spill = result.spills().stream().filter(s -> s.fromFeatureId() == upper.id() && s.toFeatureId() == lower.id())
.findFirst().orElseThrow(() -> new AssertionError("missing inter-pool cascade"));
check(spill.flowPath().getFirst().equals(spill.source()), "cascade root must be retained basin water");
check(spill.flowPath().stream().anyMatch(p -> result.cells().stream().anyMatch(c ->
c.featureId() == lower.id() && c.x() == p.x() && c.z() == p.z() && p.y() > c.bedY() && p.y() <= c.waterY())),
"cascade must actually reach the lower retained pool");
}
var openings = new HashSet<Position>(); result.spills().forEach(spill -> openings.add(spill.outlet()));
for (var cell : result.cells()) {
check(cell.waterY() - cell.bedY() >= 1 && cell.waterY() - cell.bedY() <= CavernTerraces.MAX_DEPTH, "shallow lower depth");
check(cell.carveTop() - cell.bedY() <= CavernTerraces.MAX_CARVE, "limited excavation");
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++)
check(density.sample(cell.x(), y, cell.z()) > 0, "natural foundation and sediment support");
boolean roof = false;
for (int y = cell.carveTop() + 5; y < 384; y++) roof |= density.sample(cell.x(), y, cell.z()) > 0;
check(roof, "cavity roof must remain natural");
for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) {
int x = cell.x() + d[0], z = cell.z() + d[1];
for (int y = cell.bedY() + 1; y <= cell.waterY(); y++) {
final int sy = y;
boolean adjacentWater = result.cells().stream().anyMatch(c -> c.x() == x && c.z() == z
&& c.waterY() == cell.waterY() && sy > c.bedY() && sy <= c.waterY());
check(adjacentWater || density.sample(x, y, z) > 0 || openings.contains(new Position(x,y,z)),
"every exterior fluid face must be natural rock or an explicitly declared outlet");
}
}
}
for (var spill : result.spills()) {
var path = spill.flowPath();
for (int i = 1; i < path.size(); i++) {
var a = path.get(i - 1); var b = path.get(i);
check(Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()) + Math.abs(a.z() - b.z()) == 1 && b.y() <= a.y(),
"gravity connection must traverse adjacent non-ascending voxels");
}
}
}
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,112 @@
package fr.koka.sanctuary.worldgen;
import java.util.HashSet;
/** Pure geometry regressions; actual fluid propagation is covered separately in Minecraft. */
public final class CavernLavaSmoke {
private CavernLavaSmoke() {}
public static void main(String[] args) {
CavernHydrology.Plan water = CavernHydrology.create(0, (x, y, z) -> -1);
CavernLavaDeposit.Plan empty = CavernLavaDeposit.create(0, (x, y, z) -> -1, water);
check(empty.cells().isEmpty() && empty.falls().isEmpty(), "void cannot create lava sites");
check(CavernLavaFlows.create(0, (x, y, z) -> 1, water, empty).isEmpty(),
"sealed rock cannot create an outlet");
CavernHydrology.Sampler wall = (x, y, z) ->
x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 48 && y <= 200 ? 1 : -1;
var plan = CavernLavaDeposit.create(42, wall, water);
check(plan.falls().size() == 2, "two natural wall outlets expected on reference wall");
check(plan.falls().equals(CavernLavaDeposit.create(42, wall, water).falls()), "same seed must reproduce all sites");
check(!plan.falls().equals(CavernLavaDeposit.create(43, wall, water).falls()), "seed must affect selection");
check(plan.lavaCells().size() == 2 && plan.cells().size() == 2,
"each seep writes only its rock source, never a filled falling column");
for (var cell : plan.cells()) check(wall.sample(cell.x(), cell.y(), cell.z()) > 0,
"every changed cell must start as natural rock");
var supports = new HashSet<>(plan.supports());
for (var support : supports) check(wall.sample(support.x(), support.y(), support.z()) > 0,
"source protection must be existing rock");
for (var fall : plan.falls()) {
validate(fall, wall);
check(fall.flowBounds().minY() == 0, "reference wall must allow a cascade into the void");
check(!supports.contains(fall.source()), "source itself must not be an unchanged support");
check(plan.fallsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).contains(fall),
"each source belongs to its own chunk");
check(plan.cellsInChunk(fall.source().x() >> 4, fall.source().z() >> 4).stream()
.anyMatch(c -> c.x() == fall.source().x() && c.y() == fall.source().y() && c.z() == fall.source().z()),
"source chunk must receive its lava cell");
}
check(plan.flowSources().size() == 2 && plan.flowBounds().size() == 2, "flow metadata must cover every source");
// The shelf extends well beyond the three-block lava reach. A water-like reach of seven
// would incorrectly predict a spill over its edge, rather than the small retained puddle.
CavernHydrology.Sampler shelf = (x, y, z) -> {
boolean upperWall = x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 120 && y <= 200;
boolean lowerShelf = x >= -216 && x <= -24 && Math.abs(z) <= 216 && y == 100;
return upperWall || lowerShelf ? 1 : -1;
};
var shelfFalls = CavernLavaFlows.create(42, shelf, water, empty);
check(!shelfFalls.isEmpty(), "reference shelf must receive lava");
for (var fall : shelfFalls) {
validate(fall, shelf);
check(fall.flowBounds().minY() == 101, "lava must stop on a wide natural shelf within reach three");
}
check(CavernLavaFlows.HORIZONTAL_REACH == 3, "non-ultrawarm lava reach");
lowerPocketPriority(water);
System.out.println("CavernLavaSmoke: seeded deep rock sources, existing supports, chunk ownership and three-block lava reach passed.");
}
private static void lowerPocketPriority(CavernHydrology.Plan water) {
CavernHydrology.Sampler layeredCliff = (x, y, z) -> {
if (Math.hypot(x, z) >= 220 || y < 64) return -1;
if (x <= 0) return y <= 260 ? 1 : -1;
return y <= 120 || y >= 200 && y <= 220 ? 1 : -1;
};
var plan = CavernLavaDeposit.create(0, layeredCliff, water);
var entry = plan.access().orElseThrow(() -> new AssertionError("reference cave should contain a pocket"));
check(entry.y() == 121, "lower cave pocket must take priority over the available upper ledge");
check(layeredCliff.sample(entry.x(), 220, entry.z()) > 0, "lower access must retain its natural ceiling");
check(plan.lavaCells().size() - plan.falls().size() == 18, "lower pocket keeps exactly eighteen sources");
for (int y = entry.y(); y < entry.y() + 3; y++) check(layeredCliff.sample(entry.x(), y, entry.z()) <= 0,
"lower pocket entrance must have three blocks of natural headroom");
for (int y = entry.y() - 3; y < entry.y(); y++) check(layeredCliff.sample(entry.x(), y, entry.z()) > 0,
"lower pocket entrance must stand on three natural blocks");
var changed = new HashSet<CavernLavaDeposit.Position>();
for (var cell : plan.cells()) {
check(layeredCliff.sample(cell.x(), cell.y(), cell.z()) > 0, "lower chamber only removes existing rock");
changed.add(new CavernLavaDeposit.Position(cell.x(), cell.y(), cell.z()));
}
for (var support : plan.supports()) {
check(layeredCliff.sample(support.x(), support.y(), support.z()) > 0 && !changed.contains(support),
"lower pocket shell must remain natural and unchanged");
}
CavernHydrology.Sampler upperOnly = (x, y, z) -> Math.hypot(x, z) < 220
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
var fallback = CavernLavaDeposit.create(0, upperOnly, water);
check(fallback.access().orElseThrow().y() == 201, "existing upper ledge remains a fallback when no lower site fits");
check(fallback.lavaCells().size() - fallback.falls().size() == 18, "fallback keeps its original pocket volume");
}
private static void validate(CavernLavaFlows.Fall fall, CavernHydrology.Sampler sampler) {
check(fall.source().y() >= 40 && fall.source().y() <= 160, "source altitude");
check(sampler.sample(fall.source().x(), fall.source().y(), fall.source().z()) > 0, "rock source");
check(sampler.sample(fall.outlet().x(), fall.outlet().y(), fall.outlet().z()) <= 0, "naturally open outlet");
var path = fall.flowPath();
check(path.getFirst().equals(fall.source()) && path.get(1).equals(fall.outlet()), "explicit source/outlet path");
check(fall.source().y() - path.getLast().y() >= 12, "genuine falling path");
for (int i = 1; i < path.size(); i++) {
var a = path.get(i - 1); var b = path.get(i);
check(Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()) + Math.abs(a.z() - b.z()) == 1,
"flow path must use adjacent voxels");
check(b.y() <= a.y(), "lava cannot ascend");
check(sampler.sample(b.x(), b.y(), b.z()) <= 0, "flow must stay outside natural rock");
var bounds = fall.flowBounds();
check(b.x() >= bounds.minX() && b.x() <= bounds.maxX() && b.y() >= bounds.minY()
&& b.y() <= bounds.maxY() && b.z() >= bounds.minZ() && b.z() <= bounds.maxZ(), "path within declared bounds");
}
}
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,36 @@
package fr.koka.sanctuary.worldgen;
/** Pure climate invariants; actual decor and abundance are measured in engine diagnostics. */
public final class CavernPaletteSmoke {
public static void main(String[] args) {
int oldSulfur = 0, newSulfur = 0, lush = 0, dripstone = 0;
for (int y : new int[]{64, 112, 160, 176, 184, 216, 260}) {
for (int v = -100; v <= 100; v += 2) for (int m = -100; m <= 100; m += 2) {
float variation = v / 100F, moisture = m / 100F;
var old = RiftBiomePalette.select(y, variation, moisture);
var current = CavernBiomePalette.select(y, variation, moisture);
if (old == RiftBiomePalette.Kind.SULFUR_DEPTHS) oldSulfur++;
if (current == CavernBiomePalette.Kind.SULFUR_DEPTHS) {
newSulfur++;
require(y < 176, "Sulfur must remain below the upper woodlands");
}
if (current == CavernBiomePalette.Kind.LUSH_CAVES) lush++;
if (current == CavernBiomePalette.Kind.DRIPSTONE_CAVES) dripstone++;
if (y >= 184 && old != RiftBiomePalette.Kind.SULFUR_DEPTHS)
require(current.name().equals(old.name()), "Existing upper forest climate must remain unchanged");
}
}
require(newSulfur > 0 && newSulfur * 8 < oldSulfur, "The broad sulfur province must become localized pockets");
require(lush > 0 && dripstone > 0, "Both lower cave climates must have a real selection domain");
require(CavernBiomePalette.select(112, 0.1F, 0.5F) == CavernBiomePalette.Kind.LUSH_CAVES,
"Moist lower cavities must select lush caves");
require(CavernBiomePalette.select(112, -0.1F, -0.5F) == CavernBiomePalette.Kind.DRIPSTONE_CAVES,
"Drier lower cavities must select dripstone caves");
System.out.println("CavernPaletteSmoke passed: sulfur climate cells " + oldSulfur + " -> " + newSulfur
+ "; lush=" + lush + ", dripstone=" + dripstone + " (uniform climate grid, not island abundance)");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
+4 -1
View File
@@ -51,11 +51,14 @@ Pour produire un fichier importable par un lanceur compatible Modrinth :
```sh
cd build/packwiz # depuis la racine du dépôt
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.7.mrpack
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.9.mrpack
```
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
Il ne publie rien sur Modrinth.
Lalpha.9 reste en préparation : produire cet export après la réussite du
build de cette version, sans réimporter une nouvelle instance pour mettre
à jour linstance Sanctuary Beta existante.
Utiliser l'URL HTTP affichée de `pack.toml` dans packwiz-installer. Le serveur
local doit rester actif pendant l'installation. Ce n'est pas une publication
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.8"
version = "0.1.0-alpha.9"
pack-format = "packwiz:1.1.0"
[index]