Add rifts, river basins and stratified depths for alpha.8
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 00:29:34 +02:00
parent d50d47e149
commit 80e78e0421
85 changed files with 7977 additions and 56 deletions
+28
View File
@@ -1,5 +1,33 @@
# Changelog
## 0.1.0-alpha.8 — 2026-09-09
- Forêts de surface conservées, avec de nouvelles failles courbes qui retirent
de la roche pour ouvrir lintérieur de l’île.
- Nappes de tuf, cobblestone, pierre moussue et boue compactée dans les étages
humides ; ardoise des abîmes, roche noire et basalte plus bas. Les poches de
soufre et de cinabre peuvent apparaître à des altitudes plus accessibles.
- Davantage dessais de petits filons de charbon, fer et cuivre, y compris
au contact de lair. Ajout de l’émeraude et adaptation des huit familles de
minerais aux nouvelles roches, sans quota de remplissage.
- Décoration des corniches revue avec de gros champignons, des petits
champignons plus dispersés et des sols de mousse, podzol et mycélium.
- Bassins plus grands et recherche dune
longue rivière calme au tracé arrondi, de largeur variable, soutenue par la
roche, avec bassins terminaux, berges progressives et source en paroi.
- Recherche prioritaire de la poche de lave couverte entre Y=64 et Y=160,
avec recherche plus haute si aucun site profond ne convient. Les sources
profondes supplémentaires produisent leurs coulées par les ticks vanilla.
- Nouvelle clé `sanctuary:sanctuary_rift` ; les anciennes générations jusqu’à
lalpha.7 restent séparées. Cet incrément nécessite un **nouveau monde Sanctuary**.
Validation : tests serveur réussis sur chacune des graines `0`, `42` et
`8675309`, avec les bassins terminaux, la continuité des champignons et
1 800 ticks réels de fluides. `check build assemblePack` réussi sur la graine
`42`. Les mesures et leurs limites figurent dans [Validation](docs/testing.md).
Les synchronisations packwiz sont vérifiées séparément lors de la distribution.
Aucune rivière, coulée ou réserve de ressources nest garantie pour chaque graine.
## 0.1.0-alpha.7 — 2026-09-08
- Forêts de chênes plus denses, y compris au cœur et sur les hauteurs de l’île,
+27 -18
View File
@@ -23,14 +23,16 @@ décrits dans la vision ne sont pas encore implémentés.
- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île
flottante finie, un terrain naturel et un extérieur vide.
- Île tempérée aux teintes douces : forêts de chênes et de bouleaux, clairières
fleuries et affleurements de pierre, andésite, diorite et granite. Les corniches
intérieures accueillent chênes noirs et champignons ; les essences rares
restent ponctuelles. Les
formes de terrain reprennent le générateur 26.2, adapté à l'API 26.3.
- Petits filons de charbon, fer, cuivre, or, redstone, lapis et diamant à des
altitudes adaptées au terrain relevé. Charbon, fer et cuivre peuvent aussi
affleurer sur les roches de surface et les corniches. Les stocks se constatent
après génération.
fleuries et affleurements de pierre, andésite, diorite et granite. Des failles
courbes ouvrent le terrain flottant issu du générateur 26.2. Les corniches
humides accueillent chênes noirs et champignons ; les essences rares restent
ponctuelles.
- Géologie enrichie en profondeur : tuf, pierres moussues et boue compactée
dans les intérieurs humides, puis ardoise des abîmes, roche noire, basalte et
poches de soufre. Les volumes existants fournissent leur support.
- Petits filons de charbon, fer, cuivre, or, redstone, lapis, diamant et émeraude
à des altitudes adaptées au terrain. Les essais de charbon, fer et cuivre
exposés sont plus fréquents. Les stocks se constatent après génération.
- Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires
et les choix ultérieurs de spawn ne sont pas remplacés.
- Nouveaux mondes sur 384 blocs (`Y=0..383`), île relevée de 64 blocs,
@@ -39,12 +41,15 @@ décrits dans la vision ne sont pas encore implémentés.
- Étangs, petits lacs, plages plus larges et dépôts de
trois à cinq blocs de sable, gravier, pierre ou terre enherbée. La canne à
sucre apparaît lorsque les rives respectent ses règles vanilla.
- Les courts ruisseaux imposés sont retirés ; l’île garde ses étangs et petits
lacs. La grande rivière traversante reste à développer.
- Recherche de bassins plus grands et dune longue rivière calme au tracé
arrondi, avec largeur variable et berges progressives. La liaison de deux
bassins, sa source et sa cascade entrante sont contrôlées dans les mondes
de test de lalpha.8.
- Quelques sources dans des niches rocheuses naturelles, y compris aux étages
inférieurs : Minecraft fait couler leur eau en cascades, qui peuvent descendre jusque dans le vide.
- Une petite poche de lave couverte, accessible depuis une corniche naturelle,
peut fournir la matière nécessaire à lobsidienne lorsque le relief convient.
- Une petite poche de lave couverte est recherchée en priorité sur une corniche
profonde, puis plus haut si nécessaire. Une à deux sources profondes
supplémentaires peuvent former des coulées, vérifiées par les ticks du moteur.
L'île a une emprise nominale de 512 blocs de diamètre, avec une bordure sculptée
par des bruits liés à la seed. Aucune masse centrale n'est ajoutée pour forcer
@@ -52,10 +57,14 @@ 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.7 utilise de nouveaux paramètres de génération ; créer un **nouveau monde**
pour découvrir ses forêts, ses corniches humides et ses affleurements. Les sauvegardes alpha.2
à alpha.6 conservent leur génération. Dappled Forest est désormais réservé aux
futurs continents et reste présent dans les anciennes sauvegardes alpha.5. La validation de cet incrément est suivie dans [Validation](docs/testing.md).
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).
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
@@ -79,7 +88,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.7 |
| Sanctuary / pack | 0.1.0-alpha.8 |
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
@@ -106,7 +115,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.7.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.8.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 -2
View File
@@ -10,7 +10,9 @@ Le [document de vision](vision.md) conserve les idées à long terme. Ce backlog
commit. Le prototype WG-02 et l'initialisation du spawn WG-03 sont implémentés
et passent les tests serveur décrits dans [Validation](testing.md). Les essais
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 proposés.
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 a ses mesures moteur alpha.8 ; la distribution est 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.
@@ -139,7 +141,7 @@ des bassins, présence de roche élargie et sources de paroi dans les strates
inférieures. Les écoulements sont contrôlés dans le moteur et les anciennes
générations sont préservées.
**Ticket suivant — grande rivière facultative :** chercher un long parcours qui
**Suite suivie dans WG-09 — grande rivière facultative :** chercher un long parcours qui
épouse l’île, avec source, chute et bassin, rives de sable, gravier et argile.
Le démontrer sur la vraie densité Minecraft puis après décoration et ticks de
fluide, avec une graine et des coordonnées reproductibles. Les essais du
@@ -200,6 +202,56 @@ effective sur les corniches inférieures. Cela ne livre pas encore les continent
du moteur, dont les forêts, les minerais exposés et les eaux après 1 800 ticks.
Les observations et leurs limites sont consignées dans [Validation](testing.md).
### WG-09 — Failles, rivière et profondeurs — alpha.8
**Branche :** `codex/rifts-rivers-and-depths`.
**But :** ouvrir lintérieur de l’île, enrichir sa géologie et ses ressources
de survie, tout en conservant les forêts de surface appréciées en alpha.7.
**Critères dacceptation :**
- Une à trois traces de failles courbes, de longueur nominale 84 à 160 blocs
et de largeur variable, retirent uniquement de la matière du relief initial.
Leur intersection réelle avec la roche est montrée sur les graines testées.
- Les forêts de chênes et de bouleaux restent présentes en surface. Des gros
champignons, des petits champignons dispersés et des tapis de mousse, podzol
et mycélium occupent les corniches compatibles de lintérieur humide.
- Les matières changent avec la profondeur, avec des nappes de tuf, cobblestone,
pierre moussue et boue compactée, puis ardoise des abîmes, roche noire et basalte.
Des poches de soufre réellement accessibles sont observées sur le terrain.
- Charbon, fer et cuivre exposés sont plus faciles à repérer dans les relevés.
L’émeraude est ajoutée ; or, diamant, redstone et lapis sont cherchés aux
altitudes présentes. Aucun stock fixé nest réinjecté après comptage.
- Des bassins plus grands sont retenus. Une rivière facultative est démontrée
par un chemin deau continu dau moins 140 blocs, reliant deux bassins au
même niveau avec source rocheuse et cascade entrante. Le tracé est arrondi
à l’échelle de 16 à 32 blocs, avec largeur de lit variable denviron 5 à 7
blocs. Lincision du lit reste bornée à 16 blocs ; les berges de rivière
s’étagent progressivement avec au plus 12 blocs retirés, contre 2 pour les
autres rives. Le fond et les dépôts reposent sur le terrain existant.
- La poche de lave couverte est recherchée en priorité sur une corniche basse
entre Y=64 et Y=160 ; la recherche plus haute nintervient que si aucun
emplacement profond compatible nest trouvé.
- Une à deux sources profondes de lave sont recherchées lorsque le site est
compatible ; leurs coulées viennent des ticks vanilla. Les supports, les
eaux voisines et la végétation sont contrôlés après simulation réelle.
- La nouvelle clé `sanctuary:sanctuary_rift` nactive ces traitements que pour
les nouveaux mondes. Les données et comportements historiques restent séparés.
- Les graines `0`, `42` et `8675309` disposent de mesures moteur explicites,
avec fluides actifs pendant 1 800 ticks. Une absence de rivière ou de source
est rapportée honnêtement ; un bassin isolé ne valide pas une rivière.
- Après validation : build et pack vérifiés, publication immuable, deux
synchronisations du canal packwiz et conservation de la même instance Prism,
de ses sauvegardes et de ses réglages.
**Vérification moteur alpha.8 — 9 septembre 2026 :** les trois graines de
référence passent les contrôles finaux, dont les deux bassins terminaux,
les champignons connectés et 1 800 ticks de fluides. Les stocks et leurs
périmètres sont consignés dans [Validation](testing.md). Le build complet est
réussi ; publication et synchronisations sont suivies dans la procédure de
distribution. Ce ticket local nest pas une issue distante publiée.
## 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.
+8 -2
View File
@@ -19,7 +19,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.7.mrpack
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.8.mrpack
```
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
@@ -38,7 +38,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.7/sanctuary-0.1.0-alpha.7.jar
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
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
@@ -131,6 +131,12 @@ Lalpha.7 sélectionne `sanctuary:sanctuary_woodland` pour ses forêts, ses
roches variées et ses minerais affleurants. Les mondes alpha.6 gardent
`sanctuary:sanctuary_layered` ; aucune régénération de chunks nest effectuée.
Lalpha.8 sélectionne `sanctuary:sanctuary_rift` pour les failles, les grandes
rivières, les champignons géants et la géologie des profondeurs. Créer un
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.
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).
+140 -2
View File
@@ -64,7 +64,8 @@ Le framework déplace aussi le spawn vers une grille de tests située loin du ce
Le test capture donc le spawn juste avant ce déplacement, après l'initialisation
normale du monde, et examine les coordonnées absolues de l'île.
Les neuf tests Sanctuary vérifient :
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 :
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
@@ -91,7 +92,7 @@ Les neuf tests Sanctuary vérifient :
active et les minerais sont inspectés sur le terrain. Une réserve de lave
présente conserve ses 18 sources, ses supports et son accès après simulation.
9. Les anciennes clés gardent leur traitement dorigine. Seuls les nouveaux
paramètres activent les forêts et les eaux de lalpha.7.
paramètres activent le traitement ajouté pour leur génération.
La recherche de spawn parcourt l'île finie, par anneaux de quatre blocs jusqu'à
288 blocs du centre. Elle examine d'abord la hauteur brute pour éviter de
@@ -188,6 +189,143 @@ 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.8 — 9 septembre 2026
Les trois essais utilisent des mondes neufs et le preset de production
`sanctuary:sanctuary_rift`, avec Minecraft 26.3-pre-2, Java 25, Fabric Loader
0.19.5 et Fabric API 0.160.0+26.3. Les journaux propres aux graines `0`, `42`
et `8675309` terminent leurs tests requis et leur tâche Gradle sans échec.
Le build complet `check build assemblePack` est exécuté sur la graine `42`.
Il passe en 4m 57s,
avec 12 tests purs de comportement et les tests serveur.
Les exports généraux facultatifs sont désactivés ; les diagnostics ciblés et
les assertions de comportement restent actifs.
Les nouveaux contrôles exigent de vrais bassins terminaux connectés à la
rivière et la continuité physique entre chaque tige et son chapeau de gros
champignon. Les rapports antérieurs à ces contrôles ne sont pas utilisés.
| Graine | Chunks FULL | Failles / colonnes témoins | Chênes / bouleaux supérieurs | Chênes noirs inférieurs | Gros champignons | Petits sous plafond | Minerais dehors : charbon / fer / cuivre |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 246 | 3 / 3 | 331 / 249 | 19 | 22 | 273 | 488 / 170 / 124 |
| `42` | 216 | 2 / 2 | 361 / 178 | 17 | 15 | 140 | 379 / 209 / 148 |
| `8675309` | 231 | 3 / 3 | 236 / 125 | 31 | 17 | 202 | 633 / 261 / 262 |
Les mêmes échantillons montrent la coexistence de chênes noirs et de gros
champignons sur les graines `0`, `42`, `8675309`.
Les arbres sont comptés par groupes de pieds de troncs avec sol et feuilles
observés. Les gros champignons ont une tige enracinée, un chapeau connecté et
une voûte naturelle ; leurs blocs sont de nouveau contrôlés après les fluides.
Les petits champignons sous plafond ont également leurs supports naturels.
Ils ne sont pas confondus avec le sous-ensemble `actual_lower_terrace_vegetation`.
Les failles sont vérifiées sur des colonnes témoins à lintérieur des traces :
ce nombre ne mesure pas la longueur complète des canyons.
| Graine | Longueur rivière | Déplacement | Niveau Y | Colonnes deau | Colonnes des deux bassins | Sources liées | Descente liée observée |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 409,8 | 199,7 | 243 | 2 863 | 240 / 241 | 1 | 8 |
| `42` | 247,8 | 171,0 | 247 | 1 712 | 254 / 247 | 1 | 6 |
| `8675309` | 340,7 | 251,3 | 239 | 2 247 | 265 / 244 | 1 | 8 |
La longueur vient de `river_routes[].length`, somme des déplacements successifs
du chemin, diagonales comprises. Le déplacement est la distance entre ses
extrémités. Les deux bassins ont des élargissements mesurés au-delà du lit ;
chaque raccord local et larrivée réelle de la cascade sont contrôlés. Le
tracé est arrondi à l’échelle de 16 à 32 blocs, avec un lit denviron 5 à 7
blocs de large. Les limites sont de 16 blocs retirés pour le lit, 12 pour ses
berges progressives et 2 pour les autres rives, sur support naturel.
| Graine | Eau retenue | Eau hors bassins | Eau totale | Lave totale | Sources de lave | Coulées déclarées | Accès à la poche de lave |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `0` | 6 463 | 363 | 6 826 | 144 | 20 | 2 | `(-5, 106, 80)` |
| `42` | 4 899 | 358 | 5 257 | 144 | 20 | 2 | `(-35, 108, 6)` |
| `8675309` | 5 841 | 345 | 6 186 | 144 | 20 | 2 | `(-55, 132, 0)` |
Ces fluides sont lus après 1 800 ticks dans des chunks actifs, avec témoins
indépendants du fonctionnement des ticks. Les volumes retenus correspondent
exactement aux volumes attendus. Leau extérieure reste reliée aux sources
déclarées ; la lave des coulées est distinguée des sources de la poche couverte.
La poche contient 18 sources lorsquelle est présente ; ses coordonnées
daccès ne sont pas les niveaux des blocs de lave. Sa recherche privilégie
Y=64..160, avec un site plus haut possible si la recherche basse échoue.
Les coulées supplémentaires partent de sources entre Y=40 et Y=160. Ce relevé
à 1 800 ticks ne signifie pas que tout écoulement a atteint son état final.
| Graine | Sols de mousse | Sols de podzol | Sols de mycélium |
| --- | --- | --- | --- |
| `0` | 16 268 | 15 441 | 6 855 |
| `42` | 7 197 | 6 684 | 3 502 |
| `8675309` | 12 519 | 10 616 | 5 185 |
Les sols sont des colonnes inférieures naturelles réellement revêtues de ces
matières. Les proportions de placement ne sont pas imposées au relevé.
Les stocks suivants comptent les blocs dans les seuls chunks inspectés,
**avant les ticks de fluides**. Ils incluent les variantes ordinaires et
dardoise de chaque minerai. Ils ne représentent pas toute l’île et ne sont
jamais complétés pour atteindre un quota.
| Minerai | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Charbon | 7 291 | 5 738 | 4 295 |
| Fer | 2 899 | 2 330 | 1 860 |
| Cuivre | 2 229 | 1 804 | 1 416 |
| Or | 829 | 533 | 425 |
| Redstone | 819 | 534 | 432 |
| Lapis | 237 | 229 | 122 |
| Diamant | 126 | 80 | 71 |
| Émeraude | 55 | 34 | 30 |
| Matière | Graine 0 | Graine 42 | Graine 8675309 |
| --- | --- | --- | --- |
| Tuf | 562 892 | 439 519 | 264 881 |
| Cobblestone | 288 921 | 140 182 | 73 681 |
| Cobblestone moussue | 452 727 | 381 339 | 220 767 |
| Boue compactée | 192 049 | 145 758 | 101 652 |
| Ardoise des abîmes | 1 437 063 | 712 417 | 807 405 |
| Roche noire | 379 527 | 194 967 | 248 247 |
| Basalte | 549 851 | 293 938 | 137 965 |
| Soufre | 462 657 | 313 445 | 401 190 |
| Cinabre | 118 411 | 74 761 | 114 313 |
Ces stocks ne prouvent pas seuls laccessibilité de chaque bloc. Les cartes
de surfaces, les positions des minerais exposés et les témoins de corniches
complètent le relevé. Les minerais « dehors » ont une vraie face dair ouverte
vers le ciel ou une ouverture horizontale naturelle inspectée sur douze blocs.
Le tableau présente le charbon, le fer et le cuivre. Les JSON conservent aussi
les autres familles effectivement recensées dehors, comme l’émeraude lorsquelle
est observée ; le choix des colonnes ne limite pas le périmètre du diagnostic.
L’échantillon couvre les eaux et leurs voisins, des cibles écologiques et le
site facultatif darbre remarquable ; il est volontairement orienté par ces
sites, sans prétendre donner un pourcentage représentatif de l’île.
| Graine | Réentrée bouche deau | Réentrée bouche de lave |
| --- | --- | --- |
| `0` | 1 vérifiée(s) | 1 vérifiée(s) |
| `42` | 1 vérifiée(s) | 1 vérifiée(s) |
| `8675309` | 1 vérifiée(s) | 1 vérifiée(s) |
Après consommation des marqueurs de génération, le rappel du post-traitement
conserve les modifications temporaires appliquées aux bouches testées ; les
blocs témoins sont ensuite restaurés. Les 113 fichiers historiques
suivis conservent leurs SHA-256. Aucun monde personnel nest ouvert par ces tests.
Preuves locales ignorées par Git : `build/alpha8-build.log` (build complet sur
42), `build/alpha8-seed0.log`, `build/alpha8-seed42.log`,
`build/alpha8-seed8675309.log`, les JSON `rift-seed-<graine>-after-1800-ticks`
et `-postprocessing-reentry`, les cartes de canopée et de corniches et les
profils de cascades sous `mods/sanctuary/build/run/gameTest/diagnostics/`.
`build/report-alpha8-validation.py` extrait leurs champs sans remplacer les
données absentes par des résultats supposés. Le hash du JAR vérifié est
conservé dans `build/alpha8-jar-sha256.txt`.
Ces résultats portent sur les graines testées. Ils ne garantissent aucune
rivière, essence rare ou quantité de ressources sur toutes les graines.
Lappréciation visuelle du paysage et l’équilibrage en partie restent ouverts.
Les deux synchronisations packwiz et la conservation de linstance Prism
doivent être vérifiées lors de la distribution, selon [Distribution Prism](packwiz.md).
## Contrôles et validation alpha.7 — 8 septembre 2026
`WoodlandDiagnostics` reprend les preuves hydrologiques et ajoute les forêts,
+24 -6
View File
@@ -70,13 +70,23 @@ Lambiance de départ doit rester douce et tempérée, avec de véritables for
et des clairières fleuries, quelques zones plus sèches et de larges masses
rocheuses mêlant pierre, andésite, diorite et granite. Les nappes de sable et
de gravier accompagnent les eaux. Charbon, fer et cuivre doivent aussi pouvoir
affleurer sur la roche exposée pour commencer la survie.
affleurer sur la roche exposée pour commencer la survie, avec du charbon assez
repérable pour fournir les premières expériences. Les failles doivent ouvrir
des passages courbes et des vues vers les profondeurs, sans reconstruire une
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
transformer les profondeurs en grands bassins remplis. L’émeraude rejoint les
minerais à rechercher, avec lor, le diamant, la redstone et le lapis.
Les bois dominants sont le chêne, le chêne noir et le bouleau : chênes et
bouleaux en surface, chênes noirs et champignons dans lintérieur plus humide,
avec moins de mousse uniforme. Épicéa, jungle, acacia, palétuvier et chêne pâle
restent rares mais possibles ; du bambou et de rares poches de soufre peuvent
se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un
bouleaux en surface, chênes noirs et gros champignons dans lintérieur plus
humide, avec de petits champignons dispersés et des tapis variés de mousse,
podzol et mycélium. Épicéa, jungle, acacia, palétuvier et chêne pâle restent rares
mais possibles ; du bambou peut se rencontrer dans les profondeurs. Un arbre remarquable isolé peut être un
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.
@@ -86,7 +96,15 @@ en quantités modestes pour pousser à explorer les expansions lors des grands
projets. Les minerais doivent être trouvables aux altitudes du terrain. Les
stocks sont observés après génération et les ressources renouvelables gardent
leurs règles Minecraft. Le réglage précis de ces quantités reste à éprouver en
partie coopérative. L’état livré figure dans [Génération](worldgen.md).
partie coopérative. La recherche de lalpha.8 porte notamment sur une rivière
calme continue reliant deux bassins au même niveau, alimentée par une source
en paroi et sa cascade. Son tracé doit former de larges courbes, avec une
largeur variable et des berges progressives. La petite réserve de lave est
recherchée dabord dans les étages profonds ; un site plus haut reste possible
si la roche ne fournit pas de niche compatible en dessous. Ces formes restent
facultatives selon le relief ; leur existence doit être démontrée en jeu.
L’état de chaque incrément et ses limites
figurent dans [Génération](worldgen.md).
Le premier champ de recherche des expansions associe les **huit directions**
autour de Sanctuary à des signatures de température et d'humidité. En regardant
+150 -5
View File
@@ -5,6 +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).
## Créer un monde
En solo : créer un **nouveau** monde et choisir le type de monde **Sanctuary**
@@ -29,13 +34,14 @@ de test et les sauvegardes personnelles doivent rester distincts.
avec `xz_scale=0.25`, `y_scale=0.25`, `xz_factor=80`, `y_factor=160`, `smear=4`,
ainsi que son gradient supérieur. Il est échantillonné **64 blocs plus bas**
pour relever l'île entière de 64 blocs. Reliefs, surplombs, trous et fragments
dépendent de la seed Minecraft. L'ancien gradient inférieur uniforme est
dépendent de la seed Minecraft. Lalpha.8 y soustrait des failles courbes,
sans ajouter de matière. L'ancien gradient inférieur uniforme est
remplacé par la sculpture irrégulière décrite ci-dessous.
- Le terrain naturel reste libre de laisser un trou à l'origine, de former des
surplombs ou de se séparer en fragments. **Aucun noyau ellipsoïdal, cylindre ou
plateforme de départ n'est ajouté.** Au-dessus de `Y=144`, le relief de la
génération précédente est conservé dans la densité, simplement relevé de 64
blocs. Lhydrologie traite ensuite les lits et les rives près de la surface.
plateforme de départ n'est ajouté.** Le relief relevé de 64 blocs reste le
champ source ; les failles de lalpha.8 en retirent de la matière et
lhydrologie traite ensuite les lits et les rives près de la surface.
- Le pourtour est sculpté par retrait de matière à partir du rayon **128 blocs**.
Le contour de référence de 256 blocs est déformé par les bruits, entre 204 et
288 blocs ; il indique la fin de l'érosion, pas un mur de roche.
@@ -58,13 +64,152 @@ de test et les sauvegardes personnelles doivent rester distincts.
rocheuses. Les arbres et les petits filons ont besoin du terrain existant.
Aucun mod de biomes externe nest nécessaire.
Lalpha.7 associe ces biomes aux plages, aux eaux et aux petits filons décrits
Lalpha.8 associe ces biomes aux plages, aux eaux et aux filons décrits
ci-dessous. Les ressources de survie ne se limitent pas au bois et à la pierre,
mais cet incrément ne garantit pas encore une partie Minecraft complète sur
chaque seed. Laccès à chaque progression vanilla et les ressources renouvelables
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.
## Failles, géologie et ressources — génération v8
Les nouveaux mondes utilisent `sanctuary:sanctuary_rift`, la densité
`sanctuary:final_density_rift` et le biome source `sanctuary:rift_island`.
Les anciens paramètres `sanctuary`, `sanctuary_384`, `sanctuary_hydrology`,
`sanctuary_natural`, `sanctuary_layered` et `sanctuary_woodland` restent séparés.
Changer le preset ne migre pas un monde existant ; aucune régénération de
chunks ni modification de sauvegarde nest prévue par cet incrément.
### Failles et palette
`RiftShape` définit une à trois traces principales, courbes et ramifiées,
de longueur nominale 84 à 160 blocs. Leur largeur varie avec deux bruits en
trois dimensions ; le cœur ouvert est de lordre de 4 à 12 blocs, avec des
parois érodées autour. Ces nombres décrivent les traces avant leur intersection
avec la roche, pas une garantie de canyon continu de cette taille. La passe
retire seulement de la matière et laisse intactes les zones de rayon inférieur
à 20 blocs et extérieur à 224 blocs. Elle ne crée aucun socle central.
Les huit biomes `rift_oak_forest`, `rift_birch_forest`, `rift_clearing`,
`rift_dry_woodland`, `rift_rocky_heath`, `rift_dark_grove`, `rift_bamboo_grove`
et `rift_sulfur_depths` ont de nouveaux identifiants dans le namespace
`sanctuary`. Les choix climatiques de lalpha.7 sont conservés en haut. Le
soufre devient admissible dans des régions plus larges des étages inférieurs,
notamment vers Y=176, selon les bruits de variation et dhumidité. Le test de
palette prouve cette possibilité ; seule la lecture des vrais blocs peut
prouver une poche accessible sur une graine.
La végétation vanilla de surface garde ses arbres et ses teintes. La feature
`rift_groves` passe ensuite, avant `rift_shore_sugar_cane`, pour choisir les
volumes inférieurs encore libres après les arbres. Elle peut placer de gros
champignons vanilla, avec des petits champignons dispersés et des sols mêlant
mousse, podzol et mycélium. Les supports naturels et la place disponible
restent nécessaires ; un arbre remarquable peut échouer si son site est occupé.
Lordre des plantations intérieures donne la priorité aux arbres dans trois
cas sur quatre et aux gros champignons dans lautre cas, selon un choix lié
au chunk. Cela laisse des volumes disponibles aux chênes noirs. Dans les
biomes `sanctuary:rift_dark_grove` et `sanctuary:rift_bamboo_grove`, lardoise
des abîmes peut aussi fournir le support naturel des sols et bosquets
inférieurs. Son admissibilité est vérifiée au-dessus de chaque pied : seules
ces deux variantes humides autorisent ce support.
### Matières selon la profondeur
La règle `sanctuary:rift_island` ne change que la matière de blocs déjà solides.
Ses nappes utilisent un bruit 3D, avec un bruit distinct pour faire varier les
transitions de profondeur.
| Domaine | Matières |
| --- | --- |
| Masses supérieures | Pierre, granite, andésite, diorite ; sols enherbés hors affleurements |
| Intérieur des bosquets humides | Tuf, cobblestone, cobblestone moussue, boue compactée |
| Masses profondes | Ardoise des abîmes, roche noire, basalte |
| Poches soufrées | Soufre et cinabre ordinaires sur les masses existantes |
Les strates humides se mêlent aux matières hautes entre Y=192 et Y=224 dans
leurs biomes ; la transition vers les roches profondes s’étend de Y=136 à Y=168.
La profondeur sert de critère géologique : ces blocs ne signifient pas quune
source de lave se trouve forcément à proximité. Aucun bassin, spike, source
vanilla ou bloc de soufre puissant du biome Sulfur Caves nest ajouté.
Les anciens petits dépôts vanilla de granite, andésite, diorite et tuf sont
remplacés par ces nappes dans les seuls biomes `rift_*`. Les anciennes données
de biomes et leurs features ne sont pas modifiées.
### Minerais
Les features `rift_ore_*` ciblent des tags Sanctuary locaux : les pierres hautes
et humides produisent les minerais ordinaires ; ardoise des abîmes, roche noire
et basalte produisent leurs variantes dardoise. Les tags vanilla ne sont pas
élargis globalement. Le soufre et le cinabre restent des matières distinctes.
| Minerai | Taille du filon configurée | Essais principaux par chunk | Altitudes principales |
| --- | ---: | --- | --- |
| Charbon | 9 | 8 | Y=96..304 |
| Fer | 7 | 4 | Y=88..280 |
| Cuivre | 7 | 3 | Y=96..280 |
| Or | 5 | 2 | Y=88..224 |
| Redstone | 5 | 2 | Y=88..216 |
| Lapis | 4 | 1 | Y=104..240 |
| Diamant | 4 | Un essai une fois sur deux | Y=80..208 |
| Émeraude | 3 | 1 | Y=104..224 |
Les hauteurs suivent une distribution en trapèze. Charbon, fer et cuivre
reçoivent aussi respectivement 4, 2 et 2 essais sur roche supérieure exposée,
et autant de recherches de corniche depuis lair entre Y=64 et Y=288. Un essai
peut ne produire aucun bloc. Les tailles configurées ne sont ni des quantités
garanties par filon ni des quotas par île. Lexposition à lair est autorisée
pour ces trois minerais et l’émeraude ; une partie des candidats exposés est
encore écartée pour les autres minerais. Les stocks devront être relevés après
génération, avec leur emprise explicite.
### Eaux et lave
`RiftHydrology` cherche des bassins plus grands et une longue liaison calme
sur la vraie densité fissurée. Le critère de cet incrément est un chemin deau
continu dau moins 140 blocs entre deux bassins au même niveau, alimenté par
une source rocheuse et sa cascade entrante. Il ne sagit pas dune rivière
descendant continuellement en altitude. La recherche parcourt huit directions
et pondère les trajets selon le relief et un bruit ; le parcours retenu est
lissé sur une échelle de 16 à 32 blocs, puis arrondi avant sa conversion en
blocs. Le lit varie entre environ 5 et 7 blocs de largeur, hors élargissement
des bassins. Le chemin et ses marges sont à nouveau vérifiés sur la roche
après ce lissage : une courbe ne peut pas servir à franchir un trou.
La roche doit soutenir le fond et les berges. Lincision du lit reste limitée
à 16 blocs. Les berges de rivière peuvent retirer jusqu’à 12 blocs pour
rejoindre leau par paliers progressifs ; les autres rives conservent leur
limite de 2 blocs. Les dépôts de 3 à 5 couches ont toujours besoin de supports
naturels. Aucun remblai ne franchit une faille ou un trou. Si aucun parcours
compatible nest retenu, le générateur garde les bassins et sources qui
conviennent au relief.
Le calcul initial du plan reste coûteux : les essais finaux sur ce poste
mesurent 80,5 secondes pour la graine `0`, 50,7 pour `42` et 59,7 pour
`8675309`, hors génération des chunks. Le résultat est mis en cache par état
aléatoire du monde ; il nest pas recalculé pour chaque chunk. Loptimisation
de ce premier calcul reste un travail ultérieur.
La recherche de lave profonde vise une à deux petites sources entre Y=40 et
Y=160, en complément de la niche couverte accessible. Cette poche de 18 blocs
sources est recherchée dabord sur une corniche intérieure entre Y=64 et
Y=160. Si aucun site profond ne réunit les conditions de support, de voûte
et daccès, la recherche reprend sur les corniches plus hautes, jusqu’à Y=300.
Il ny a pas de poche créée de force lorsque les deux recherches échouent.
Les sources supplémentaires laissent Minecraft former leurs coulées par ses
ticks, sans colonne préremplie. Leau, les supports et les marges de végétation doivent être
contrôlés après décoration et simulation. La finition des bouches doit rester
limitée à la première génération du chunk, sans effacer un aménagement joueur
lors dun rechargement.
**Validation du 9 septembre 2026 :** le build complet réussit et les contrôles
moteur passent sur les trois graines de référence. Les failles, les bassins terminaux,
les liaisons de rivière, les champignons et les fluides après 1 800 ticks sont
documentés avec les relevés de ressources dans [Validation](testing.md).
Ces échantillons ne garantissent pas la présence de chaque ressource sur
toutes les graines. Les rapports intermédiaires ne servent pas de preuve
pour la version finale ; aucun monde personnel nest utilisé par ces tests.
## Sculpture du pourtour — conservée depuis la génération v2
La version **0.1.0-alpha.2** retire deux contraintes de la génération v1
+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.7
pack_version=0.1.0-alpha.7
mod_version=0.1.0-alpha.8
pack_version=0.1.0-alpha.8
maven_group=fr.koka.sanctuary
+32
View File
@@ -147,3 +147,35 @@ tasks.register('woodlandGroveSmoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.worldgen.WoodlandGroveSmoke'
}
tasks.named('check') { dependsOn('woodlandPaletteSmoke', 'woodlandGroveSmoke') }
tasks.register('riftShapeSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.RiftShapeSmoke'
}
tasks.named('check') { dependsOn('riftShapeSmoke') }
tasks.register('riftPaletteSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.RiftPaletteSmoke'
}
tasks.named('check') { dependsOn('riftPaletteSmoke') }
tasks.register('riftHydrologySmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.RiftHydrologySmoke'
}
tasks.named('check') { dependsOn('riftHydrologySmoke') }
tasks.register('riftLavaSmoke', JavaExec) {
group = 'verification'
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.worldgen.RiftLavaSmoke'
}
tasks.named('check') { dependsOn('riftLavaSmoke') }
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.worldgen.SanctuarySpawn;
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 net.minecraft.tags.FluidTags;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import fr.koka.sanctuary.worldgen.IslandShape;
@@ -31,7 +32,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 (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.start(helper);
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);
else HydrologyDiagnostics.start(helper);
@@ -40,7 +42,8 @@ public final class SanctuaryWorldGameTests {
@GameTest(maxTicks = 200)
public void hydrologyDoesNotActivateForSavedLegacySettings(GameTestHelper helper) {
var generator = (NoiseBasedChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
if (WoodlandHydrologyRuntime.enabled(generator)) WoodlandDiagnostics.verifyLegacyIsolation(helper);
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);
else HydrologyDiagnostics.verifyLegacyIsolation(helper);
@@ -121,13 +124,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),
|| WoodlandHydrologyRuntime.enabled(generator) || RiftHydrologyRuntime.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 (RiftHydrologyRuntime.enabled(generator)) {
boolean water = state.getFluidState().is(FluidTags.WATER)
&& RiftDiagnostics.isWithinSpringInspectionFootprint(RiftHydrologyRuntime.riftPlan(helper.getLevel()), x, z);
final int bx = x, bz = z;
boolean lava = state.getFluidState().is(FluidTags.LAVA)
&& RiftHydrologyRuntime.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;
}
helper.assertTrue(state.isAir() || state.getFluidState().is(FluidTags.WATER)
&& (WoodlandHydrologyRuntime.enabled(generator)
? WoodlandDiagnostics.isWithinSpringInspectionFootprint(WoodlandHydrologyRuntime.woodlandPlan(helper.getLevel()), x, z)
@@ -1,5 +1,9 @@
package fr.koka.sanctuary;
import fr.koka.sanctuary.worldgen.RiftIslandDensity;
import fr.koka.sanctuary.worldgen.RiftIslandBiomeSource;
import fr.koka.sanctuary.worldgen.RiftGrovesFeature;
import fr.koka.sanctuary.worldgen.RiftShoreSugarCaneFeature;
import fr.koka.sanctuary.worldgen.MainIslandDensity;
import fr.koka.sanctuary.worldgen.WoodlandIslandBiomeSource;
import fr.koka.sanctuary.worldgen.WoodlandShoreSugarCaneFeature;
@@ -37,6 +41,10 @@ public final class SanctuaryMod implements ModInitializer {
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("woodland_island"), WoodlandIslandBiomeSource.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_shore_sugar_cane"), WoodlandShoreSugarCaneFeature.CODEC);
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("woodland_groves"), WoodlandGrovesFeature.CODEC);
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("rift_island"), RiftIslandDensity.CODEC);
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);
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.worldgen.RiftSpringOutlets;
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 RiftSpringOutletMixin {
@Unique
private RiftSpringOutlets.Pending sanctuary$pendingRiftOutlets = RiftSpringOutlets.Pending.EMPTY;
@Inject(method = "postProcessGeneration", at = @At("HEAD"))
private void sanctuary$captureRiftOutlets(ServerLevel level, CallbackInfo callback) {
sanctuary$pendingRiftOutlets = RiftSpringOutlets.capturePending(level, (LevelChunk) (Object) this);
}
@Inject(method = "postProcessGeneration", at = @At("TAIL"))
private void sanctuary$finishRiftOutlets(ServerLevel level, CallbackInfo callback) {
var pending = sanctuary$pendingRiftOutlets;
sanctuary$pendingRiftOutlets = RiftSpringOutlets.Pending.EMPTY;
RiftSpringOutlets.finish(level, (LevelChunk) (Object) this, pending);
}
}
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.worldgen.HydrologyRuntime;
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 java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.Holder;
@@ -28,7 +29,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 (WoodlandHydrologyRuntime.enabled(generator)) {
if (RiftHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
RiftHydrologyRuntime.apply(generator, randomState, generated);
return generated;
}));
} else if (WoodlandHydrologyRuntime.enabled(generator)) {
callback.setReturnValue(callback.getReturnValue().thenApply(generated -> {
WoodlandHydrologyRuntime.apply(generator, randomState, generated);
return generated;
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.worldgen;
/** Alpha.8 retains the upper woodlands and brings mineral pockets into reachable lower layers. */
public final class RiftBiomePalette {
public enum Kind {
OAK_FOREST, BIRCH_FOREST, CLEARING, DRY_WOODLAND, ROCKY_HEATH,
DARK_GROVE, BAMBOO_GROVE, SULFUR_DEPTHS
}
private RiftBiomePalette() {}
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;
// A broad dry mineral province below the inhabited upper layers, with no fluid features.
if (elevation < 184 && variation > 0.16F && moisture < 0.35F) return Kind.SULFUR_DEPTHS;
// Keep alpha.7's horizontal outcrops; altitude alone never removes the upper forest.
if (variation > 0.48F) return Kind.ROCKY_HEATH;
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 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.8: 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 RiftGrovesFeature implements Feature {
public static final MapCodec<RiftGrovesFeature> CODEC = MapCodec.unit(RiftGrovesFeature::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<RiftGrovesFeature> 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 (!RiftHydrologyRuntime.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) ->
RiftHydrologyRuntime.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) || !RiftHydrologyRuntime.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) ->
RiftHydrologyRuntime.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(RiftIslandBiomeSource.DARK_GROVE);
boolean bamboo = biome.is(RiftIslandBiomeSource.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(RiftIslandBiomeSource.DARK_GROVE) && !biome.is(RiftIslandBiomeSource.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(RiftIslandBiomeSource.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(RiftIslandBiomeSource.DARK_GROVE) || biome.is(RiftIslandBiomeSource.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,883 @@
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.8 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 RiftHydrology {
public static final int RADIUS = 192;
public static final int MAX_CARVE = 16;
public static final int MAX_DEPTH = 5;
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 RiftHydrology() {}
@FunctionalInterface
public interface Sampler { float sample(int x, int y, int z); }
public enum Kind { POND, LAKE, RIVER }
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); }
}
/** 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 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 Plan(Map<Long, Cell> source, List<Feature> features, List<Spring> springs, Set<Position> springFlow, int columns, long samples) {
List<Cell> sorted = source.values().stream()
.sorted(Comparator.comparingInt(Cell::x).thenComparingInt(Cell::z)).toList();
Map<Long, Cell> ordered = new LinkedHashMap<>();
Map<Long, List<Cell>> byChunk = new HashMap<>();
for (Cell cell : sorted) {
ordered.put(key(cell.x(), cell.z()), 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);
chunks = Map.copyOf(byChunk);
this.features = List.copyOf(features);
this.springs = List.copyOf(springs);
this.springFlow = Set.copyOf(springFlow);
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 Collection<Cell> cells() { return cells.values(); }
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 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 ? 420 + (int) (random(seed, center.x(), center.z()) * 280)
: 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 = waterCells(footprint, level, id);
if (wet == null || !contained(wet)) continue;
addFeature(id, kind, level, List.of(center), wet);
break;
}
}
addRiverSpring();
addSedimentSheets();
addLowerWallSprings();
addSprings();
return new Plan(cells, features, springs, springFlow, columns.size(), samples);
}
/** 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;
}
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,181 @@
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.8 only: immutable plans cached by world state, applied to fresh terrain before decoration. */
public final class RiftHydrologyRuntime {
private static final Map<RandomState, RiftHydrology.Plan> PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private static final Map<RandomState, RiftLavaDeposit.Plan> LAVA_PLANS =
Collections.synchronizedMap(new WeakHashMap<>());
private RiftHydrologyRuntime() {}
public static boolean enabled(NoiseBasedChunkGenerator generator) {
return generator.generatorSettings().is(SanctuarySpawn.RIFT_SETTINGS);
}
public static RiftHydrology.Plan riftPlan(ServerLevel level) {
return plan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static RiftHydrology.Plan plan(NoiseBasedChunkGenerator generator, RandomState randomState) {
if (!enabled(generator)) throw new IllegalArgumentException("Rift 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 = RiftHydrology.create(state.seed(),
(x, y, z) -> state.sampleBlockValueUncached(density, x, y, z));
SanctuaryMod.LOGGER.info("Sanctuary rift 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 rift 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 RiftLavaDeposit.Plan lavaPlan(ServerLevel level) {
return lavaPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(),
level.getChunkSource().randomState());
}
public static RiftLavaDeposit.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 = RiftLavaDeposit.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(RiftHydrology.Spring::outlet)
.filter(outlet -> (outlet.x() >> 4) == chunkPos.x() && (outlet.z() >> 4) == chunkPos.z()).toList());
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 RiftHydrology.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 = RiftMaterials.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++) {
var cell = water.cellAt(x, z);
if (cell != null && 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 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("Rift hydrology differs from terrain at " + pos + " for seed " + seed);
}
}
}
@@ -0,0 +1,89 @@
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.8 rift climate, separate from all saved earlier biome-source codecs. */
public final class RiftIslandBiomeSource extends BiomeSource {
public static final ResourceKey<Biome> OAK_FOREST = key("rift_oak_forest");
public static final ResourceKey<Biome> BIRCH_FOREST = key("rift_birch_forest");
public static final ResourceKey<Biome> CLEARING = key("rift_clearing");
public static final ResourceKey<Biome> DRY_WOODLAND = key("rift_dry_woodland");
public static final ResourceKey<Biome> ROCKY_HEATH = key("rift_rocky_heath");
public static final ResourceKey<Biome> DARK_GROVE = key("rift_dark_grove");
public static final ResourceKey<Biome> BAMBOO_GROVE = key("rift_bamboo_grove");
public static final ResourceKey<Biome> SULFUR_DEPTHS = key("rift_sulfur_depths");
public static final MapCodec<RiftIslandBiomeSource> 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)
).apply(instance, RiftIslandBiomeSource::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;
public RiftIslandBiomeSource(Holder<Biome> oakForest, Holder<Biome> birchForest, Holder<Biome> clearing,
Holder<Biome> dryWoodland, Holder<Biome> rockyHeath, Holder<Biome> darkGrove,
Holder<Biome> bambooGrove, Holder<Biome> sulfurDepths) {
this.oakForest = oakForest;
this.birchForest = birchForest;
this.clearing = clearing;
this.dryWoodland = dryWoodland;
this.rockyHeath = rockyHeath;
this.darkGrove = darkGrove;
this.bambooGrove = bambooGrove;
this.sulfurDepths = sulfurDepths;
}
@Override
protected MapCodec<RiftIslandBiomeSource> codec() { return CODEC; }
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.of(oakForest, birchForest, clearing, dryWoodland, rockyHeath, darkGrove, bambooGrove, sulfurDepths);
}
@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 (RiftBiomePalette.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;
};
};
}
private static ResourceKey<Biome> key(String path) {
return ResourceKey.create(Registries.BIOME, SanctuaryMod.id(path));
}
}
@@ -0,0 +1,68 @@
package fr.koka.sanctuary.worldgen;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka.sanctuary.SanctuaryMod;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Interval;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
/** Alpha.8 wrapper. The existing raised-island density and all earlier world settings stay intact. */
public record RiftIslandDensity(DensityFunction terrain, DensityFunction warp, DensityFunction detail) implements DensityFunction {
public static final Identifier LAYOUT_RANDOM = SanctuaryMod.id("rift_layout_alpha8");
public static final MapCodec<RiftIslandDensity> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
DensityFunction.CODEC.fieldOf("terrain").forGetter(RiftIslandDensity::terrain),
DensityFunction.CODEC.fieldOf("warp").forGetter(RiftIslandDensity::warp),
DensityFunction.CODEC.fieldOf("detail").forGetter(RiftIslandDensity::detail)
).apply(instance, RiftIslandDensity::new));
@Override
public DensitySampler compileSampler(CompileContext context) {
return new RiftSampler(terrain.compileSampler(context), warp.compileSampler(context), detail.compileSampler(context),
RiftShape.layout(context.createRandom(LAYOUT_RANDOM).nextLong()));
}
/** Diagnostics read exactly the immutable layout used by the compiled field. */
public RiftShape.Layout layout(RandomState state) {
DensitySampler sampler = state.getSampler(this);
if (sampler instanceof RiftSampler rift) return rift.layout();
throw new IllegalStateException("The rift density was unexpectedly replaced by another sampler");
}
public static final class RiftSampler implements DensitySampler {
private final DensitySampler terrain, warp, detail;
private final RiftShape.Layout layout;
private RiftSampler(DensitySampler terrain, DensitySampler warp, DensitySampler detail, RiftShape.Layout layout) {
this.terrain = terrain; this.warp = warp; this.detail = detail; this.layout = layout;
}
public RiftShape.Layout layout() { return layout; }
@Override
public float sampleValue(SamplerContext context, int x, int y, int z) {
float natural = terrain.sampleValue(context, x, y, z);
if (natural <= 0 || !RiftShape.mayAffect(layout, x, z)) return natural;
return RiftShape.density(layout, x, y, z, natural,
warp.sampleValue(context, x, y, z), detail.sampleValue(context, x, y, z));
}
@Override
public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
DensitySampler.sampleVolumeNaive(context, buffer, volume, this);
}
}
@Override
public DensityFunction rewriteChildren(DfRewriteRule rule) {
return new RiftIslandDensity(rule.rewrite(terrain), rule.rewrite(warp), rule.rewrite(detail));
}
@Override public Interval range() { return Interval.of(Math.min(-1.0F, terrain.range().min()), terrain.range().max()); }
@Override public int domainAxes() { return ALL_AXES; }
@Override public MapCodec<RiftIslandDensity> codec() { return CODEC; }
}
@@ -0,0 +1,322 @@
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.8 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 RiftLavaDeposit {
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 RiftLavaDeposit() {}
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<RiftLavaFlows.Fall> falls;
private final Map<Long, List<RiftLavaFlows.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<RiftLavaFlows.Fall> falls) {
this.falls = List.copyOf(falls);
Map<Long, List<RiftLavaFlows.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<RiftLavaFlows.Fall> falls() { return falls; }
public List<RiftLavaFlows.Fall> fallsInChunk(int x, int z) { return fallChunks.getOrDefault(key(x, z), List.of()); }
public List<Position> flowSources() { return falls.stream().map(RiftLavaFlows.Fall::source).toList(); }
public List<RiftHydrology.FlowBounds> flowBounds() { return falls.stream().map(RiftLavaFlows.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, RiftHydrology.Sampler sampler, RiftHydrology.Plan hydrology) {
Plan pocket = new Search(seed, sampler, hydrology).find();
List<RiftLavaFlows.Fall> falls = RiftLavaFlows.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 RiftHydrology.Sampler sampler;
private final RiftHydrology.Plan hydrology;
private final Map<Long, Integer> heights = new HashMap<>();
Search(long seed, RiftHydrology.Sampler sampler, RiftHydrology.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;
}
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 RiftHydrology.FlowBounds(x - 10, level - 4, z - 10, x + 10, level + 10, z + 10);
for (var cell : hydrology.cells()) {
var ground = new RiftHydrology.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;
}
return true;
}
private static long distanceSquared(RiftHydrology.FlowBounds a, RiftHydrology.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,238 @@
package fr.koka.sanctuary.worldgen;
import fr.koka.sanctuary.worldgen.RiftLavaDeposit.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 RiftLavaFlows {
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 RiftLavaFlows() {}
/** 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,
RiftHydrology.FlowBounds flowBounds, List<Position> supports) {
public Fall { flowPath = List.copyOf(flowPath); supports = List.copyOf(supports); }
}
public static List<Fall> create(long seed, RiftHydrology.Sampler sampler,
RiftHydrology.Plan water, RiftLavaDeposit.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, RiftHydrology.FlowBounds bounds) {}
private static final class Search {
private final long seed;
private final RiftHydrology.Sampler sampler;
private final RiftHydrology.Plan water;
private final RiftLavaDeposit.Plan pocket;
private final List<Fall> falls = new ArrayList<>();
Search(long seed, RiftHydrology.Sampler sampler, RiftHydrology.Plan water, RiftLavaDeposit.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) {
RiftHydrology.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(RiftHydrology.FlowBounds bounds) {
for (var cell : water.cells()) {
var column = new RiftHydrology.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;
}
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 RiftHydrology.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.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 RiftHydrology.FlowBounds bounds(Position at) {
return new RiftHydrology.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(RiftHydrology.FlowBounds a, RiftHydrology.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,43 @@
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.8 shore deposits: broad continuous patches through the existing sediment thickness. */
public final class RiftMaterials {
private RiftMaterials() {}
public static BlockState sedimentBlock(RiftHydrology.Cell cell, long seed, int depth) {
if (cell.material() == RiftHydrology.ShoreMaterial.GRASS) {
return (depth == 0 ? Blocks.GRASS_BLOCK : Blocks.DIRT).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,130 @@
package fr.koka.sanctuary.worldgen;
import java.util.ArrayList;
import java.util.List;
/** Alpha.8 fracture geometry. All operations remove material from the existing island field. */
public final class RiftShape {
public static final int INNER_UNTOUCHED_RADIUS = 20;
public static final int OUTER_UNTOUCHED_RADIUS = 224;
private static final double TWO_PI = Math.PI * 2.0;
public record Point(double x, double z) {}
public record Segment(double x, double z, double dx, double dz, double halfLength,
double halfWidth, double bend, double phase) {}
public record Rift(long id, Segment main, List<Segment> branches) {
public Rift { branches = List.copyOf(branches); }
}
public record Layout(List<Rift> rifts) {
public Layout { rifts = List.copyOf(rifts); }
}
private RiftShape() {}
/** Seed is obtained from Minecraft's named compile-context random stream. */
public static Layout layout(long seed) {
List<Rift> rifts = new ArrayList<>();
int count = 1 + (int) Math.floorMod(mix(seed + 1), 3);
double rotation = unit(seed + 2) * TWO_PI;
for (int i = 0; i < count; i++) {
long salt = mix(seed + 0x51F71L + i * 0x9e3779b97f4a7c15L);
double bearing = rotation + i * TWO_PI / count + (unit(salt) - 0.5) * 0.42;
double radius = 76 + unit(salt + 1) * 60;
// Tangential placement leaves most coast and the small central neighbourhood intact.
// This protects existing material; it does not create a spawn island or solid core.
double x = Math.rint(Math.cos(bearing) * radius), z = Math.rint(Math.sin(bearing) * radius);
double angle = bearing + Math.PI / 2 + (unit(salt + 2) - 0.5) * 0.72;
double length = 84 + unit(salt + 3) * 76;
Segment main = new Segment(x, z, Math.cos(angle), Math.sin(angle), length / 2,
2.8 + unit(salt + 4) * 1.7, 6 + unit(salt + 5) * 7, unit(salt + 6) * TWO_PI);
List<Segment> branches = new ArrayList<>();
int branchCount = 1 + (int) Math.floorMod(mix(salt + 7), 2);
for (int branch = 0; branch < branchCount; branch++) {
double along = (branch == 0 ? -0.25 : 0.27) * length;
Point anchor = centerline(main, along);
double branchAngle = angle + (branch == 0 ? -1 : 1) * (0.40 + unit(salt + 8 + branch) * 0.35);
double halfLength = 12 + unit(salt + 12 + branch) * 13;
double dx = Math.cos(branchAngle), dz = Math.sin(branchAngle);
Segment raw = new Segment(anchor.x() + dx * halfLength, anchor.z() + dz * halfLength,
dx, dz, halfLength, 2.2 + unit(salt + 16 + branch) * 0.7,
2 + unit(salt + 20 + branch) * 3, unit(salt + 24 + branch) * TWO_PI);
// Translate the curved start back onto the parent; no isolated artificial shafts.
Point start = centerline(raw, -halfLength);
branches.add(new Segment(raw.x() + anchor.x() - start.x(), raw.z() + anchor.z() - start.z(),
dx, dz, halfLength, raw.halfWidth(), raw.bend(), raw.phase()));
}
rifts.add(new Rift(salt, main, branches));
}
return new Layout(rifts);
}
public static Point centerline(Segment segment, double along) {
double bend = curve(segment, Math.clamp(along, -segment.halfLength(), segment.halfLength()));
return new Point(segment.x() + segment.dx() * along - segment.dz() * bend,
segment.z() + segment.dz() * along + segment.dx() * bend);
}
/** A cheap conservative bound avoids sampling two extra noises throughout the whole island. */
public static boolean mayAffect(Layout layout, int x, int z) {
double radius = Math.hypot((double) x, z);
if (radius <= INNER_UNTOUCHED_RADIUS || radius >= OUTER_UNTOUCHED_RADIUS) return false;
for (Rift rift : layout.rifts()) {
if (near(rift.main(), x, z)) return true;
for (Segment branch : rift.branches()) if (near(branch, x, z)) return true;
}
return false;
}
/** warped and detail are bounded 3D noise samples. The vertical core stays open even at
* maximum perturbation, while ledges and side chambers vary along the rock faces. */
public static float density(Layout layout, int x, int y, int z, float terrain, float warped, float detail) {
if (terrain <= 0 || !mayAffect(layout, x, z)) return terrain;
double coarse = Math.clamp(warped, -1.0F, 1.0F), fine = Math.clamp(detail, -1.0F, 1.0F);
double erosion = 0;
for (Rift rift : layout.rifts()) {
erosion = Math.max(erosion, erosion(rift.main(), x, z, coarse, fine));
for (Segment branch : rift.branches()) erosion = Math.max(erosion, erosion(branch, x, z, coarse, fine));
}
double radius = Math.hypot((double) x, z);
erosion *= smoothstep((radius - INNER_UNTOUCHED_RADIUS) / 16.0)
* smoothstep((OUTER_UNTOUCHED_RADIUS - radius) / 20.0);
return (float) Math.max(-1.0, terrain - erosion);
}
private static boolean near(Segment segment, int x, int z) {
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
return Math.abs(rx * segment.dx() + rz * segment.dz()) <= segment.halfLength() + 10
&& Math.abs(-rx * segment.dz() + rz * segment.dx()) <= segment.bend() * 1.3 + 10;
}
private static double erosion(Segment segment, int x, int z, double coarse, double fine) {
if (!near(segment, x, z)) return 0;
double rx = (double) x - segment.x(), rz = (double) z - segment.z();
double along = rx * segment.dx() + rz * segment.dz();
double end = Math.clamp(along, -segment.halfLength(), segment.halfLength());
double sideways = -rx * segment.dz() + rz * segment.dx() - curve(segment, end);
sideways -= coarse * 1.05 + fine * 0.30;
double width = Math.clamp(segment.halfWidth() + Math.sin(end / 13.0 + segment.phase()) * 0.65
+ Math.sin(end / 5.7 + segment.phase() * 1.3) * 0.35 + coarse * 0.65 + fine * 0.2, 2.3, 5.7);
double distance = Math.hypot(sideways, along - end);
double core = Math.max(2.3, width - 1.0), outer = width + 1.6;
return 2.1 * smoothstep((outer - distance) / (outer - core));
}
private static double curve(Segment segment, double along) {
double t = along / (segment.halfLength() * 2.0);
return segment.bend() * (Math.sin(t * Math.PI * 2 + segment.phase()) - Math.sin(segment.phase())) * 0.5
+ segment.bend() * 0.23 * Math.sin(t * Math.PI * 6);
}
private static double smoothstep(double value) {
double t = Math.clamp(value, 0, 1);
return t * t * (3 - 2 * t);
}
private static double unit(long seed) { return (mix(seed) >>> 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,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 RiftShoreSugarCaneFeature implements Feature {
public static final MapCodec<RiftShoreSugarCaneFeature> CODEC = MapCodec.unit(RiftShoreSugarCaneFeature::new);
@Override
public MapCodec<RiftShoreSugarCaneFeature> codec() {
return CODEC;
}
@Override
public boolean place(WorldGenLevel level, ChunkGenerator generator, RandomSource random, BlockPos origin) {
if (!(generator instanceof NoiseBasedChunkGenerator noise) || !RiftHydrologyRuntime.enabled(noise)) return false;
var plan = RiftHydrologyRuntime.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,104 @@
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.8 outlet finishing, using vanilla's persisted and consumed generation queue. */
public final class RiftSpringOutlets {
public record Pending(List<RiftHydrology.Position> sources, List<RiftHydrology.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 RiftSpringOutlets() {}
/** 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)
|| !RiftHydrologyRuntime.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 = RiftHydrologyRuntime.plan(noise, level.getChunkSource().randomState());
List<RiftHydrology.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 fall : RiftHydrologyRuntime.lavaPlan(noise, level.getChunkSource().randomState()).falls()) {
var source = new RiftHydrology.Position(fall.source().x(), fall.source().y(), fall.source().z());
var outlet = new RiftHydrology.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)
|| !RiftHydrologyRuntime.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 = RiftHydrologyRuntime.plan(noise, random).cellAt(outlet.x(), 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, RiftHydrology.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, RiftHydrology.Position position) {
return (position.x() >> 4) == chunk.getPos().x() && (position.z() >> 4) == chunk.getPos().z();
}
private static BlockPos blockPos(RiftHydrology.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);
}
}
@@ -30,6 +30,9 @@ public final class SanctuarySpawn {
public static final ResourceKey<NoiseGeneratorSettings> WOODLAND_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_woodland"));
public static final ResourceKey<NoiseGeneratorSettings> RIFT_SETTINGS =
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary_rift"));
private SanctuarySpawn() {}
public static boolean usesSanctuaryGenerator(ServerLevel level) {
@@ -39,7 +42,8 @@ public final class SanctuarySpawn {
|| generator.generatorSettings().is(HYDROLOGY_SETTINGS)
|| generator.generatorSettings().is(NATURAL_SETTINGS)
|| generator.generatorSettings().is(LAYERED_SETTINGS)
|| generator.generatorSettings().is(WOODLAND_SETTINGS));
|| generator.generatorSettings().is(WOODLAND_SETTINGS)
|| generator.generatorSettings().is(RIFT_SETTINGS));
}
/** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */
@@ -19,5 +19,13 @@
"biome.sanctuary.woodland_rocky_heath": "Rocky Heath",
"biome.sanctuary.woodland_dark_grove": "Dark Undergrowth",
"biome.sanctuary.woodland_bamboo_grove": "Bamboo Grove",
"biome.sanctuary.woodland_sulfur_depths": "Sulfur Depths"
"biome.sanctuary.woodland_sulfur_depths": "Sulfur Depths",
"biome.sanctuary.rift_oak_forest": "Sanctuary Oak Woodland",
"biome.sanctuary.rift_birch_forest": "Sanctuary Birch Woodland",
"biome.sanctuary.rift_clearing": "Flowered Clearing",
"biome.sanctuary.rift_dry_woodland": "Dry Woodland",
"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"
}
@@ -19,5 +19,13 @@
"biome.sanctuary.woodland_rocky_heath": "Lande rocheuse",
"biome.sanctuary.woodland_dark_grove": "Sous-bois sombre",
"biome.sanctuary.woodland_bamboo_grove": "Bosquet de bambous",
"biome.sanctuary.woodland_sulfur_depths": "Profondeurs soufrées"
"biome.sanctuary.woodland_sulfur_depths": "Profondeurs soufrées",
"biome.sanctuary.rift_oak_forest": "Chênaie de Sanctuary",
"biome.sanctuary.rift_birch_forest": "Bétulaie de Sanctuary",
"biome.sanctuary.rift_clearing": "Clairière fleurie",
"biome.sanctuary.rift_dry_woodland": "Bois sec",
"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"
}
@@ -0,0 +1,7 @@
{
"values": [
"minecraft:deepslate",
"minecraft:blackstone",
"minecraft:basalt"
]
}
@@ -0,0 +1,6 @@
{
"values": [
"#sanctuary:rift_upper_ore_replaceables",
"#sanctuary:rift_deep_ore_replaceables"
]
}
@@ -0,0 +1,12 @@
{
"values": [
"minecraft:stone",
"minecraft:granite",
"minecraft:diorite",
"minecraft:andesite",
"minecraft:tuff",
"minecraft:cobblestone",
"minecraft:mossy_cobblestone",
"minecraft:packed_mud"
]
}
@@ -0,0 +1,166 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,161 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,163 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,152 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -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
},
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.75
}
@@ -0,0 +1,165 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,157 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.7
}
@@ -0,0 +1,155 @@
{
"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:rift_groves",
"sanctuary:rift_shore_sugar_cane"
],
[
"minecraft:freeze_top_layer"
]
],
"has_precipitation": true,
"temperature": 0.8
}
@@ -0,0 +1,16 @@
{
"type": "sanctuary:rift_island",
"terrain": "sanctuary:final_density_384",
"warp": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_warp",
"xz_scale": 1.0,
"y_scale": 1.0
},
"detail": {
"type": "minecraft:noise",
"noise": "sanctuary:rift_detail",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0,
"size": 9,
"targets": [
{
"state": "minecraft:coal_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_coal_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0,
"size": 7,
"targets": [
{
"state": "minecraft:copper_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_copper_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.25,
"size": 4,
"targets": [
{
"state": "minecraft:diamond_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_diamond_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0,
"size": 3,
"targets": [
{
"state": "minecraft:emerald_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_emerald_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.15,
"size": 5,
"targets": [
{
"state": "minecraft:gold_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_gold_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0,
"size": 7,
"targets": [
{
"state": "minecraft:iron_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_iron_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.15,
"size": 4,
"targets": [
{
"state": "minecraft:lapis_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_lapis_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,21 @@
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.15,
"size": 5,
"targets": [
{
"state": "minecraft:redstone_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_upper_ore_replaceables"
}
},
{
"state": "minecraft:deepslate_redstone_ore",
"target": {
"predicate_type": "minecraft:tag_match",
"tag": "sanctuary:rift_deep_ore_replaceables"
}
}
]
}
@@ -0,0 +1,37 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_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:rift_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:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_deep_strata"
}
},
"sanctuary:rift_upper_strata"
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_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:rift_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:rift_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,271 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:rift_sulfur_depths"
},
"then_run": "sanctuary:rift_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:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_deep_strata"
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:rift_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:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:rift_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:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": 0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_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:rift_depth_boundary",
"min_threshold": -1000,
"max_threshold": -0.45,
"is_3d": true
},
"then_run": "sanctuary:rift_humid_strata"
}
}
]
}
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:rift_rocky_heath"
},
"then_run": "sanctuary:rift_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_exposure",
"min_threshold": 0.85,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:rift_upper_strata"
},
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:biome",
"biome_is": "sanctuary:rift_dry_woodland"
},
"then_run": {
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_exposure",
"min_threshold": 0.15,
"max_threshold": 1000,
"is_3d": false
},
"then_run": "sanctuary:rift_upper_strata"
}
},
"sanctuary:starter_island",
"sanctuary:rift_upper_strata"
]
}
@@ -0,0 +1,34 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_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:rift_strata",
"min_threshold": -0.55,
"max_threshold": -0.3,
"is_3d": true
},
"then_run": {
"type": "minecraft:block",
"result_state": "minecraft:cinnabar"
}
},
"sanctuary:rift_depth_strata"
]
}
@@ -0,0 +1,51 @@
{
"type": "minecraft:sequence",
"sequence": [
{
"type": "minecraft:condition",
"if_true": {
"type": "minecraft:noise_threshold",
"noise": "sanctuary:rift_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:rift_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:rift_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_octave": -3,
"octave_count": 2,
"amplitude_modifiers": [1.0, 0.5]
}
@@ -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,5 @@
{
"base_octave": -5,
"octave_count": 3,
"amplitude_modifiers": [1.0, 0.5, 0.25]
}
@@ -0,0 +1,23 @@
{
"default_block": "minecraft:stone",
"default_fluid": "minecraft:air",
"disable_mob_generation": false,
"legacy_random_source": true,
"material_rule": "sanctuary:rift_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:rift_groves"
},
"placement": []
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_coal",
"placement": [
{
"type": "minecraft:count",
"count": 8
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 96
},
"max_inclusive": {
"absolute": 304
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,53 @@
{
"feature": "sanctuary:rift_ore_coal",
"placement": [
{
"type": "minecraft:count",
"count": 4
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"min_inclusive": {
"absolute": 64
},
"max_inclusive": {
"absolute": 288
}
}
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:environment_scan",
"direction_of_search": "down",
"max_steps": 32,
"target_condition": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables"
},
"allowed_search_condition": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:offset",
"x": 0,
"y": 1,
"z": 0
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,31 @@
{
"feature": "sanctuary:rift_ore_coal",
"placement": [
{
"type": "minecraft:count",
"count": 4
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:heightmap",
"heightmap": "OCEAN_FLOOR_WG"
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables",
"offset": [
0,
-1,
0
]
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_copper",
"placement": [
{
"type": "minecraft:count",
"count": 3
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 96
},
"max_inclusive": {
"absolute": 280
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,53 @@
{
"feature": "sanctuary:rift_ore_copper",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"min_inclusive": {
"absolute": 64
},
"max_inclusive": {
"absolute": 288
}
}
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:environment_scan",
"direction_of_search": "down",
"max_steps": 32,
"target_condition": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables"
},
"allowed_search_condition": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:offset",
"x": 0,
"y": 1,
"z": 0
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,31 @@
{
"feature": "sanctuary:rift_ore_copper",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:heightmap",
"heightmap": "OCEAN_FLOOR_WG"
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables",
"offset": [
0,
-1,
0
]
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_diamond",
"placement": [
{
"type": "minecraft:rarity_filter",
"chance": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 80
},
"max_inclusive": {
"absolute": 208
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_emerald",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 104
},
"max_inclusive": {
"absolute": 224
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_gold",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 88
},
"max_inclusive": {
"absolute": 224
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_iron",
"placement": [
{
"type": "minecraft:count",
"count": 4
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 88
},
"max_inclusive": {
"absolute": 280
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,53 @@
{
"feature": "sanctuary:rift_ore_iron",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"min_inclusive": {
"absolute": 64
},
"max_inclusive": {
"absolute": 288
}
}
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:environment_scan",
"direction_of_search": "down",
"max_steps": 32,
"target_condition": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables"
},
"allowed_search_condition": {
"type": "minecraft:matching_block_tag",
"tag": "minecraft:air"
}
},
{
"type": "minecraft:offset",
"x": 0,
"y": 1,
"z": 0
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,31 @@
{
"feature": "sanctuary:rift_ore_iron",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:heightmap",
"heightmap": "OCEAN_FLOOR_WG"
},
{
"type": "minecraft:block_predicate_filter",
"predicate": {
"type": "minecraft:matching_block_tag",
"tag": "sanctuary:rift_ore_replaceables",
"offset": [
0,
-1,
0
]
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_lapis",
"placement": [
{
"type": "minecraft:count",
"count": 1
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 104
},
"max_inclusive": {
"absolute": 240
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,27 @@
{
"feature": "sanctuary:rift_ore_redstone",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:trapezoid",
"min_inclusive": {
"absolute": 88
},
"max_inclusive": {
"absolute": 216
}
}
},
{
"type": "minecraft:biome"
}
]
}
@@ -0,0 +1,6 @@
{
"feature": {
"type": "sanctuary:rift_shore_sugar_cane"
},
"placement": []
}
@@ -0,0 +1,23 @@
{
"feature": "minecraft:trees_birch_and_oak_leaf_litter",
"placement": [
{
"type": "minecraft:count",
"count": 2
},
{
"type": "minecraft:in_square"
},
{
"type": "minecraft:surface_water_depth_filter",
"max_water_depth": 0
},
{
"type": "minecraft:heightmap",
"heightmap": "OCEAN_FLOOR"
},
{
"type": "minecraft:biome"
}
]
}
@@ -5,17 +5,17 @@
"generator": {
"type": "minecraft:noise",
"biome_source": {
"type": "sanctuary:woodland_island",
"oak_forest": "sanctuary:woodland_oak_forest",
"birch_forest": "sanctuary:woodland_birch_forest",
"clearing": "sanctuary:woodland_clearing",
"dry_woodland": "sanctuary:woodland_dry_woodland",
"dark_grove": "sanctuary:woodland_dark_grove",
"bamboo_grove": "sanctuary:woodland_bamboo_grove",
"sulfur_depths": "sanctuary:woodland_sulfur_depths",
"rocky_heath": "sanctuary:woodland_rocky_heath"
"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"
},
"settings": "sanctuary:sanctuary_woodland"
"settings": "sanctuary:sanctuary_rift"
}
},
"minecraft:the_end": {
@@ -6,7 +6,8 @@
"InitialSpawnMixin",
"SurfaceHydrologyMixin",
"LayeredSpringOutletMixin",
"WoodlandSpringOutletMixin"
"WoodlandSpringOutletMixin",
"RiftSpringOutletMixin"
],
"client": [
"client.SanctuarySkyMixin",
@@ -0,0 +1,30 @@
package fr.koka.sanctuary.worldgen;
/** Behavioral fixture: a thick uneven plateau can host a real connected river; void cannot. */
public final class RiftHydrologySmoke {
public static void main(String[] args) {
RiftHydrology.Sampler plateau = (x, y, z) -> {
int top = 248 + (int) (Math.sin(x / 33.0) * 4 + Math.cos(z / 41.0) * 4);
return x * x + z * z < 205 * 205 && y >= 100 && y <= top ? 1 : -1;
};
var plan = RiftHydrology.create(42, plateau);
var river = plan.features().stream().filter(f -> f.kind() == RiftHydrology.Kind.RIVER).findFirst()
.orElseThrow(() -> new AssertionError("A supported plateau must permit a continuous river"));
if (river.path().size() < 140) throw new AssertionError("River is too short");
for (var point : river.path()) {
var cell = plan.cellAt(point.x(), point.z());
if (cell == null || !cell.hasWater() || cell.featureId() != river.id()) throw new AssertionError("Dry gap in river");
}
for (var cell : plan.cells()) {
for (int y = cell.bedY() - cell.sedimentDepth() - 1; y <= cell.bedY(); y++)
if (plateau.sample(cell.x(), y, cell.z()) <= 0) throw new AssertionError("Artificial foundation");
if (cell.carveTop() - cell.bedY() > RiftHydrology.MAX_CARVE) throw new AssertionError("Excavation exceeds budget");
}
var replay = RiftHydrology.create(42, plateau);
if (!plan.features().equals(replay.features()) || !java.util.List.copyOf(plan.cells()).equals(java.util.List.copyOf(replay.cells())))
throw new AssertionError("Same seed and terrain must replay exactly");
var empty = RiftHydrology.create(42, (x,y,z) -> -1);
if (!empty.cells().isEmpty() || !empty.springs().isEmpty()) throw new AssertionError("Void must remain void");
System.out.println("Rift hydrology: connected river, natural supports, bounded carving, deterministic replay and empty void passed; river=" + river.path().size() + ", wet=" + river.waterCells());
}
}
@@ -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 RiftLavaSmoke {
private RiftLavaSmoke() {}
public static void main(String[] args) {
RiftHydrology.Plan water = RiftHydrology.create(0, (x, y, z) -> -1);
RiftLavaDeposit.Plan empty = RiftLavaDeposit.create(0, (x, y, z) -> -1, water);
check(empty.cells().isEmpty() && empty.falls().isEmpty(), "void cannot create lava sites");
check(RiftLavaFlows.create(0, (x, y, z) -> 1, water, empty).isEmpty(),
"sealed rock cannot create an outlet");
RiftHydrology.Sampler wall = (x, y, z) ->
x <= -32 && x >= -176 && Math.abs(z) <= 152 && y >= 48 && y <= 200 ? 1 : -1;
var plan = RiftLavaDeposit.create(42, wall, water);
check(plan.falls().size() == 2, "two natural wall outlets expected on reference wall");
check(plan.falls().equals(RiftLavaDeposit.create(42, wall, water).falls()), "same seed must reproduce all sites");
check(!plan.falls().equals(RiftLavaDeposit.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.
RiftHydrology.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 = RiftLavaFlows.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(RiftLavaFlows.HORIZONTAL_REACH == 3, "non-ultrawarm lava reach");
lowerPocketPriority(water);
System.out.println("RiftLavaSmoke: seeded deep rock sources, existing supports, chunk ownership and three-block lava reach passed.");
}
private static void lowerPocketPriority(RiftHydrology.Plan water) {
RiftHydrology.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 = RiftLavaDeposit.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<RiftLavaDeposit.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 RiftLavaDeposit.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");
}
RiftHydrology.Sampler upperOnly = (x, y, z) -> Math.hypot(x, z) < 220
&& y >= 100 && y <= (x <= 0 ? 240 : 200) ? 1 : -1;
var fallback = RiftLavaDeposit.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(RiftLavaFlows.Fall fall, RiftHydrology.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,49 @@
package fr.koka.sanctuary.worldgen;
import java.util.EnumSet;
/** Alpha.8 regression: richer reachable depths retain the complete alpha.7 upper climate. */
public final class RiftPaletteSmoke {
public static void main(String[] args) {
for (int y = 216; y < 384; y += 4) {
for (int variation = -20; variation <= 20; variation++) {
for (int moisture = -20; moisture <= 20; moisture++) {
float v = variation / 20.0F, m = moisture / 20.0F;
require(RiftBiomePalette.select(y, v, m).name()
.equals(WoodlandBiomePalette.select(y, v, m).name()),
"The established upper woodland palette must remain unchanged");
}
}
}
require(RiftBiomePalette.select(176, 0.25F, 0.1F) == RiftBiomePalette.Kind.SULFUR_DEPTHS,
"Sulfur must reach the lower ledges, rather than only the very bottom of the dimension");
require(WoodlandBiomePalette.select(176, 0.25F, 0.1F) != WoodlandBiomePalette.Kind.SULFUR_DEPTHS,
"The former saved climate must keep its original lower palette");
require(RiftBiomePalette.select(144, -0.2F, 0.2F) == RiftBiomePalette.Kind.DARK_GROVE,
"Humid woodland must remain available beside the mineral province");
require(RiftBiomePalette.select(144, -0.6F, 0.7F) == RiftBiomePalette.Kind.BAMBOO_GROVE,
"Rare bamboo pockets must remain possible in the humid depths");
EnumSet<RiftBiomePalette.Kind> kinds = EnumSet.noneOf(RiftBiomePalette.Kind.class);
int oldSulfur = 0, newSulfur = 0;
for (int y = 64; y < 216; y += 8) {
for (int variation = -10; variation <= 10; variation++) {
for (int moisture = -10; moisture <= 10; moisture++) {
float v = variation / 10.0F, m = moisture / 10.0F;
RiftBiomePalette.Kind selected = RiftBiomePalette.select(y, v, m);
kinds.add(selected);
if (selected == RiftBiomePalette.Kind.SULFUR_DEPTHS) newSulfur++;
if (WoodlandBiomePalette.select(y, v, m) == WoodlandBiomePalette.Kind.SULFUR_DEPTHS) oldSulfur++;
}
}
}
require(kinds.equals(EnumSet.allOf(RiftBiomePalette.Kind.class)),
"All eight palettes must remain reachable within the island's range");
require(newSulfur > oldSulfur * 4,
"The sulfur climate must be substantially more accessible without a per-world quota");
System.out.println("RiftPaletteSmoke: preserved upper forests, reachable sulfur and humid lower palettes passed.");
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
@@ -0,0 +1,46 @@
package fr.koka.sanctuary.worldgen;
/** Subtraction, vertical passage and bounded geometry without Minecraft or a world save. */
public final class RiftShapeSmoke {
public static void main(String[] args) {
for (long seed : new long[]{0, 42, 8675309, Long.MIN_VALUE, Long.MAX_VALUE}) {
var layout = RiftShape.layout(seed);
require(layout.equals(RiftShape.layout(seed)), "The same seed must reproduce identical fractures");
require(layout.rifts().size() >= 1 && layout.rifts().size() <= 3, "There should be only a few major fractures");
int originalSolid = 0, removed = 0;
for (int x = -288; x <= 288; x += 2) for (int z = -288; z <= 288; z += 2) {
float input = Math.hypot(x, z) < 220 ? 0.65F : -1.0F;
float output = RiftShape.density(layout, x, 200, z, input,
(float) Math.sin(x / 31.0 + z / 19.0), (float) Math.cos(x / 7.0 - z / 11.0));
require(output <= input, "Fractures must never add terrain");
require(Float.isFinite(output), "Density must stay finite");
if (input < 0) require(output == input, "Existing void must be preserved exactly");
if (Math.hypot(x, z) >= RiftShape.OUTER_UNTOUCHED_RADIUS || Math.hypot(x, z) <= RiftShape.INNER_UNTOUCHED_RADIUS)
require(output == input, "Keep the far coast and central neighbourhood unchanged");
if (input > 0) { originalSolid++; if (output <= 0) removed++; }
}
require(removed > 100, "Fractures must actually cut a solid volume");
require(removed < originalSolid * 0.09, "Most of the existing island mass must be preserved");
for (var rift : layout.rifts()) {
var center = RiftShape.centerline(rift.main(), 0);
int x = (int) Math.round(center.x()), z = (int) Math.round(center.z());
require(rift.main().halfLength() * 2 >= 80 && rift.main().halfLength() * 2 <= 160, "Major fracture length is bounded");
for (int y = 1; y < 384; y++) for (float warp : new float[]{-1, 1}) for (float detail : new float[]{-1, 1}) {
require(RiftShape.density(layout, x, y, z, 1, warp, detail) < 0,
"A real continuous vertical core must cross even maximally dense rock");
}
for (var branch : rift.branches()) {
var start = RiftShape.centerline(branch, -branch.halfLength());
require(RiftShape.density(layout, (int) Math.round(start.x()), 170, (int) Math.round(start.z()), 1, 0, 0) < 0,
"A branch must join its parent fracture without a detached drilled shaft");
}
}
}
require(!RiftShape.layout(0).equals(RiftShape.layout(42)), "The world seed must change fracture locations");
var layout = RiftShape.layout(0);
for (int x : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE}) for (int z : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE})
require(RiftShape.density(layout, x, 128, z, -1, 1, -1) == -1, "Extreme coordinates must preserve void without overflow");
System.out.println("RiftShapeSmoke: subtractive branching fractures, through-passages and preserved island mass passed.");
}
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.7"
version = "0.1.0-alpha.8"
pack-format = "packwiz:1.1.0"
[index]